-
Notifications
You must be signed in to change notification settings - Fork 20
/
ts-ast-util.ts
191 lines (169 loc) · 7.91 KB
/
ts-ast-util.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
/**
* TypeScript 语法树的操作工具
*
* 语法树 Spec: https://ts-morph.com/
*/
import type { Node, MethodDeclaration, ShorthandPropertyAssignment, PropertyAssignment, ImportDeclaration, ClassDeclaration, SourceFile, ObjectLiteralExpression } from 'ts-morph'
import { TypeGuards, SyntaxKind } from 'ts-morph'
import debugFactory from 'debug'
import { TagName } from '../models/component-info'
import { componentID, ComponentReference } from '../models/component-reference'
const debug = debugFactory('ts-ast-util')
export function getSanImportDeclaration (sourceFile: SourceFile): ImportDeclaration | undefined {
return sourceFile.getImportDeclaration(
node => node.getModuleSpecifierValue() === 'san'
)
}
/**
* import {Component as OtherName} from 'san';
* 获取到 “OtherName”
*/
export function getComponentClassIdentifier (sourceFile: SourceFile): string | undefined {
const declaration = getSanImportDeclaration(sourceFile)
if (!declaration) return
const namedImports = declaration.getNamedImports()
for (const namedImport of namedImports) {
const name = namedImport.getName()
if (name !== 'Component') continue
const alias = namedImport.getAliasNode()
if (alias) return alias.getText()
return 'Component'
}
}
export function isChildClassOf (clazz: ClassDeclaration, parentClass: string) {
const extendClause = clazz.getHeritageClauseByKind(SyntaxKind.ExtendsKeyword)
if (!extendClause) return false
const typeNode = extendClause.getTypeNodes().find(x => x.getText() === parentClass)
if (!typeNode) return false
return true
}
export function getPropertyStringValue<T extends string> (clazz: ClassDeclaration, memberName: string, defaultValue: T): T;
export function getPropertyStringValue<T extends string> (clazz: ClassDeclaration, memberName: string): T | undefined;
export function getPropertyStringValue<T extends string> (clazz: ClassDeclaration, memberName: string, defaultValue?: T): T | undefined {
const member = clazz.getProperty(memberName)
if (!member) return defaultValue
const init = member.getInitializer()
if (!init) return defaultValue
// 字符串常量,取其字面值
const value = getLiteralText(init)
if (value !== undefined) return value as T
// 变量,找到定义处,取其字面值(非字面量跑错)
if (TypeGuards.isIdentifier(init)) {
const identName = init.getText()
const file = clazz.getSourceFile()
const decl = file.getVariableDeclarationOrThrow(identName)
const value = decl.getInitializer()
if (!value) throw new Error(`${JSON.stringify(decl.getParent().getText())} not supported, specify a string literal for "${memberName}"`)
const str = getLiteralText(value)
if (str === undefined) {
throw new Error(`${JSON.stringify(value.getText())} not supported, specify a string literal for "${memberName}"`)
}
return str as T
}
throw new Error(`invalid "${memberName}" property`)
}
export function getPropertyStringArrayValue<T extends string[]> (clazz: ClassDeclaration, memberName: string): T | undefined {
const member = clazz.getProperty(memberName)
if (!member) return undefined
const init = member.getInitializer()
if (!init) return undefined
if (!TypeGuards.isArrayLiteralExpression(init)) {
throw new Error(`invalid "${memberName}": "${init.getText()}", array literal expected`)
}
return init.getElements().map(element => getLiteralText(element)) as T
}
function getLiteralText (expr: Node) {
if (TypeGuards.isStringLiteral(expr) || TypeGuards.isNoSubstitutionTemplateLiteral(expr)) {
return expr.getLiteralValue()
}
}
export function getChildComponents (
clazz: ClassDeclaration,
defaultClassDeclaration: ClassDeclaration | undefined,
getComponentClassFromObjectLiteral: (rawObjectExpr: ObjectLiteralExpression) => ClassDeclaration
): Map<TagName, ComponentReference> {
const member = clazz.getProperty('components')
const ret: Map<TagName, ComponentReference> = new Map()
if (!member) return ret
// 对引入的名称做索引,例如
//
// import XList from './list'
// 索引为
// 'XList' => { specifier: './list', named: false }
const file = clazz.getSourceFile()
const importedNames: Map<string, { specifier: string, named: boolean }> = new Map()
for (const decl of file.getImportDeclarations()) {
const specifier = decl.getModuleSpecifier().getLiteralValue()
const defaultImport = decl.getDefaultImport()
if (defaultImport) {
importedNames.set(defaultImport.getText(), { specifier, named: false })
}
for (const namedImport of decl.getNamedImports()) {
importedNames.set(namedImport.getName(), { specifier, named: true })
}
}
// 子组件声明遍历,例如
//
// components: {
// 'x-list': XList
// }
// 解析后的子组件信息为
// 'x-list' => { specifier: './list', id: '0' }
const init = member.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression)
for (const prop of init.getProperties()) {
if (!TypeGuards.isPropertyAssignment(prop)) throw new Error(`${JSON.stringify(prop.getText())} not supported`)
const propName = getPropertyAssignmentName(prop)
// 判断是否为 'self' 使用自己作为组件
// 用法见 https://baidu.github.io/san/tutorial/component/#components
const propStringValue = prop.getInitializerIfKind(SyntaxKind.StringLiteral)
if (propStringValue) {
if (propStringValue.getLiteralValue() !== 'self') {
throw new Error(`Invalid component for ${propName}`)
}
ret.set(propName, new ComponentReference(
'.',
componentID(clazz.isDefaultExport(), clazz.getName()!)
))
continue
}
const propObjectValue = prop.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression)
if (propObjectValue) {
const clazz = getComponentClassFromObjectLiteral(propObjectValue)
ret.set(propName, new ComponentReference(
'.',
clazz.getName()!
))
continue
}
const childComponentClassName = prop.getInitializerIfKindOrThrow(SyntaxKind.Identifier).getText()
if (importedNames.has(childComponentClassName)) { // 子组件来自外部源文件
const { specifier, named } = importedNames.get(childComponentClassName)!
ret.set(propName, new ComponentReference(
specifier,
componentID(!named, childComponentClassName)
))
} else { // 子组件来自当前源文件
const isDefault = !!defaultClassDeclaration && defaultClassDeclaration.getName() === childComponentClassName
ret.set(propName, new ComponentReference(
'.',
componentID(isDefault, childComponentClassName)
))
}
}
return ret
}
export function getObjectLiteralPropertyKeys (clazz: ClassDeclaration, propertyName: string): string[] {
const prop = clazz.getProperty(propertyName)
if (!prop) return []
const init = prop.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression)
return init.getProperties().map(prop => {
if (TypeGuards.isPropertyAssignment(prop)) return getPropertyAssignmentName(prop)
if (TypeGuards.isShorthandPropertyAssignment(prop)) return getPropertyAssignmentName(prop)
if (TypeGuards.isMethodDeclaration(prop)) return getPropertyAssignmentName(prop)
throw new Error('object property not recognized')
})
}
export function getPropertyAssignmentName (prop: PropertyAssignment | ShorthandPropertyAssignment | MethodDeclaration) {
const nameNode = prop.getNameNode()
return TypeGuards.isStringLiteral(nameNode) ? nameNode.getLiteralValue() : prop.getName()
}