16
import * as ts from "typescript"
17
import { GenericVisitor } from "./options"
18
import * as path from "path"
21
getDeclarationsByNode,
24
isCommonMethodOrSubclass,
28
import { LinterWhitelist } from "./LinterWhitelist"
30
export enum LinterError {
34
COMPUTED_PROPERTY_NAME,
40
UNSUPPORTED_TYPE_PARAMETER,
41
PARAMETER_INITIALIZER,
50
INTERFACE_METHOD_TYPE_INCONSISTENT_WITH_PARENT,
51
USE_COMPONENT_AS_PARAM,
59
export interface LinterMessage {
67
const suppressed = new Set([LinterError.UNION_CONTAINS_ENUM])
69
function stringMessage(message: LinterMessage): string {
70
return `${message.pos} - [${LinterError[message.error]}] ${message.message}`
73
let allInterfaces = new Map<string, string>()
75
export class LinterVisitor implements GenericVisitor<LinterMessage[]> {
76
private output: LinterMessage[] = []
78
constructor(private sourceFile: ts.SourceFile, private typeChecker: ts.TypeChecker) {
81
visitWholeFile(): LinterMessage[] {
82
ts.forEachChild(this.sourceFile, (node) => this.visit(node))
86
visit(node: ts.Node): void {
87
if (ts.isClassDeclaration(node)) {
89
} else if (ts.isInterfaceDeclaration(node)) {
90
this.visitInterface(node)
91
} else if (ts.isModuleDeclaration(node)) {
92
this.visitNamespace(node)
93
} else if (ts.isEnumDeclaration(node)) {
95
} else if (ts.isFunctionDeclaration(node)) {
96
this.visitFunctionDeclaration(node)
97
} else if (ts.isTypeAliasDeclaration(node)) {
98
this.visitTypeAlias(node)
102
visitNamespace(node: ts.ModuleDeclaration) {
104
this.report(node, LinterError.NAMESPACE, `Namespace detected: ${asString(node.name)}`)
106
ts.forEachChild(node, this.visit)
109
visitClass(clazz: ts.ClassDeclaration): void {
110
this.checkClassDuplicate(clazz)
111
const allInheritCount = clazz.heritageClauses
112
?.map(it => it.types)
115
if (allInheritCount > 1) {
116
this.report(clazz, LinterError.MULTIPLE_INHERITANCE, `Multiple inheritance for class ${asString(clazz.name)}`)
118
if (clazz.members.every(ts.isConstructorDeclaration) && allInheritCount == 0) {
119
this.report(clazz, LinterError.INCORRECT_DATA_CLASS, `Data class ${identName(clazz.name)} declared wrong way: use class/interface with fields`)
121
clazz.members.forEach(child => {
122
if (ts.isConstructorDeclaration(child)) {
123
this.visitConstructor(child)
124
} else if (ts.isMethodDeclaration(child)) {
125
this.visitMethod(child)
126
} else if (ts.isPropertyDeclaration(child)) {
127
this.visitProperty(child)
130
this.checkClassInheritance(clazz)
131
this.interfaceOrClassChecks(clazz)
134
checkClassDuplicate(clazz: ts.InterfaceDeclaration | ts.ClassDeclaration) {
135
let clazzName = asString(clazz.name)
136
if (allInterfaces.has(clazzName)) {
137
this.report(clazz, LinterError.DUPLICATE_INTERFACE,
138
`Duplicate interface ${clazzName}: ${clazz.getSourceFile().fileName} and ${allInterfaces.get(clazzName)}`)
140
allInterfaces.set(clazzName, clazz.getSourceFile().fileName)
143
visitInterface(clazz: ts.InterfaceDeclaration): void {
144
this.checkClassDuplicate(clazz)
145
const allInheritCount = clazz.heritageClauses
146
?.map(it => it.types)
149
if (allInheritCount > 1) {
150
this.report(clazz, LinterError.MULTIPLE_INHERITANCE, `Multiple inheritance for interface ${asString(clazz.name)}`)
152
clazz.modifiers?.forEach(it => {
153
if (it.kind == ts.SyntaxKind.PrivateKeyword) {
154
this.report(clazz, LinterError.PRIVATE_VISIBILITY, `Private visibility is useless: ${clazz.getText(this.sourceFile).substring(0, 50)}`)
157
clazz.members.forEach(child => {
158
if (ts.isConstructSignatureDeclaration(child)) {
159
this.visitConstructor(child)
160
} else if (ts.isMethodSignature(child)) {
161
this.visitMethod(child)
162
} else if (ts.isPropertySignature(child)) {
163
this.visitProperty(child)
164
} else if (ts.isCallSignatureDeclaration(child)) {
165
this.visitMethod(child)
168
this.interfaceOrClassChecks(clazz)
171
checkType(type: ts.TypeNode | undefined): void {
173
if (type.kind == ts.SyntaxKind.AnyKeyword) {
174
let parent = type.parent
175
this.report(type, LinterError.ANY_KEYWORD, `Keyword "any" is disallowed: ${parent.getText()}`)
178
if (ts.isArrayTypeNode(type)) {
179
this.checkType(type.elementType)
182
if (ts.isTypeLiteralNode(type)) {
183
this.report(type, LinterError.TYPE_LITERAL, `Type literal`)
184
type.members.forEach(it => {
185
if (ts.isPropertySignature(it)) this.visitProperty(it)
186
if (ts.isIndexSignatureDeclaration(it)) {
187
this.report(it, LinterError.INDEX_SIGNATURE, `Index signature type: ${type.getText()}`)
188
it.parameters.forEach(it => this.checkType(it.type))
193
if (ts.isUnionTypeNode(type)) {
194
const enumType = findEnumType(type.types, this.typeChecker)
195
if (enumType != undefined) {
196
this.report(type, LinterError.UNION_CONTAINS_ENUM, `Union: '${type.getText()}' contains type Enum: '${enumType.name.text}'`)
198
type.types.forEach(it => {
202
if (ts.isTypeReferenceNode(type)) {
203
if (this.inParamCheck) {
204
const declarations = getDeclarationsByNode(this.typeChecker, type.typeName)
205
if (declarations.length > 0 && ts.isClassDeclaration(declarations[0])
206
&& isCommonMethodOrSubclass(this.typeChecker, declarations[0])) {
207
this.report(type, LinterError.USE_COMPONENT_AS_PARAM, `Component ${identName(declarations[0].name)} used as parameter`)
210
if (ts.isQualifiedName(type.typeName)) {
211
this.report(type, LinterError.TYPE_ELEMENT_TYPE,
212
`Type element types unsupported, use type "${ts.idText(type.typeName.left as ts.Identifier)}" itself: ${type.getText(this.sourceFile)}`)
215
if (ts.isParenthesizedTypeNode(type)) {
216
this.checkType(type.type)
218
if (ts.isTupleTypeNode(type)) {
219
this.report(type, LinterError.TUPLE_TYPE, `Tuple type: ${type.getText(this.sourceFile)}`)
220
type.elements.forEach(it => this.checkType(it))
222
if (ts.isIndexedAccessTypeNode(type)) {
223
this.report(type, LinterError.INDEXED_ACCESS_TYPE, `Indexed access type: ${type.getText(this.sourceFile)}`)
224
this.checkType(type.indexType)
225
this.checkType(type.objectType)
227
if (ts.isTemplateLiteralTypeNode(type)) {
228
this.report(type, LinterError.TEMPLATE_LITERAL, `Template literal: ${type.getText(this.sourceFile)}`)
230
if (ts.isImportTypeNode(type)) {
231
this.report(type, LinterError.IMPORT_TYPE, `Import type: ${type.getText(this.sourceFile)}`)
233
if (this.isTypeParameterReferenceAndNotCommonMethod(type)) {
234
this.report(type, LinterError.UNSUPPORTED_TYPE_PARAMETER, `Unsupported type parameter: ${type.getText(this.sourceFile)}`)
238
isTypeParameterReferenceAndNotCommonMethod(type: ts.TypeNode): boolean {
239
if (!ts.isTypeReferenceNode(type)) return false
240
const name = type.typeName
241
const declaration = getDeclarationsByNode(this.typeChecker, name)[0]
242
if (!declaration) return false
243
if (ts.isTypeParameterDeclaration(declaration)) {
244
let parent = declaration.parent
245
if (ts.isClassDeclaration(parent)) {
246
return !isCommonMethodOrSubclass(this.typeChecker, parent)
252
checkName(name: ts.PropertyName | undefined): void {
254
if (ts.isComputedPropertyName(name)) {
255
this.report(name, LinterError.COMPUTED_PROPERTY_NAME, `Computed property name ${name.getText(this.sourceFile)}`)
257
let nameString = identName(name)!
258
if (LinterVisitor.cppKeywords.has(nameString)) {
261
LinterError.CPP_KEYWORDS,
262
`Use C/C++ keyword as the field name: ${nameString}`
269
private static cppKeywords = new Set([
270
`alignas`, `alignof`, `and`,
271
`and_eq`, `asm`, `atomic_cancel`, `atomic_commit`,
272
`atomic_noexcept`, `auto`, `bitand`, `bitor`, `bool`,
273
`break`, `case`, `catch`, `char`, `char8_t`, `char16_t`,
274
`char32_t`, `class`, `compl`, `concept`, `const`, `consteval`,
275
`constexpr`, `constinit`, `const_cast`, `continue`, `co_await`,
276
`co_return`, `co_yield`, `decltype`, `default`, `delete`, `do`,
277
`double`, `dynamic_cast`, `else`, `enum`, `explicit`, `export`,
278
`extern`, `false`, `float`, `for`, `friend`, `goto`, `if`,
279
`inline`, `int`, `long`, `mutable`, `namespace`, `new`, `noexcept`,
280
`not`, `not_eq`, `nullptr`, `operator`, `or`, `or_eq`, `private`,
281
`protected`, `public`, `reflexpr`, `register`, `reinterpret_cast`,
282
`requires`, `return`, `short`, `signed`,
283
`sizeof`, `static`, `static_assert`, `static_cast`,
284
`struct`, `switch`, `synchronized`, `template`,
285
`this`, `thread_local`, `throw`, `true`, `try`,
286
`typedef`, `typeid`, `typename`, `union`,
287
`unsigned`, `using`, `virtual`, `void`,
288
`volatile`, `wchar_t`, `while`, `xor`,
292
visitConstructor(ctor: ts.ConstructorDeclaration | ts.ConstructSignatureDeclaration): void {
293
ctor.parameters.map(param => this.checkType(param.type))
296
visitMethod(method: ts.MethodDeclaration | ts.MethodSignature | ts.CallSignatureDeclaration): void {
297
this.checkType(method.type)
298
method.modifiers?.forEach(it => {
299
if (it.kind == ts.SyntaxKind.PrivateKeyword) {
300
this.report(method, LinterError.PRIVATE_VISIBILITY, `Private visibility is useless: Private visibility is useless: ${method.getText(this.sourceFile).substring(0, 50)}`)
303
method.parameters.forEach(it => this.visitParameter(it))
306
private inParamCheck = false
307
visitParameter(parameter: ts.ParameterDeclaration): void {
308
if (parameter.initializer) {
309
this.report(parameter, LinterError.PARAMETER_INITIALIZER, "Parameter initializer is forbidden")
311
this.inParamCheck = true
312
this.checkType(parameter.type)
313
this.inParamCheck = false
317
visitProperty(property: ts.PropertySignature | ts.PropertyDeclaration): void {
318
property.modifiers?.forEach(it => {
319
if (it.kind == ts.SyntaxKind.PrivateKeyword) {
320
this.report(property, LinterError.PRIVATE_VISIBILITY, `Private visibility is useless: ${property.getText(this.sourceFile)}`)
323
this.checkType(property.type)
324
this.checkName(property.name)
327
visitEnum(enumDeclaration: ts.EnumDeclaration): void {
328
enumDeclaration.members.forEach(member => {
329
if (member.initializer && !ts.isNumericLiteral(member.initializer)) {
332
LinterError.ENUM_WITH_INIT,
333
`Enum ${nameOrNullForIdl(enumDeclaration.name)}.${nameOrNullForIdl(member.name)} with non-int initializer: ${member.initializer.getText(this.sourceFile)}`
339
visitTypeAlias(type: ts.TypeAliasDeclaration): void {
340
this.checkType(type.type)
343
visitFunctionDeclaration(functionDeclaration: ts.FunctionDeclaration): void {
344
this.report(functionDeclaration, LinterError.TOP_LEVEL_FUNCTIONS, `Top level function: ${functionDeclaration.getText(this.sourceFile)}`)
345
functionDeclaration.parameters.forEach(it => this.visitParameter(it))
348
report(node: ts.Node, error: LinterError, message: string): void {
349
if (suppressed.has(error)) return
351
file: this.sourceFile,
352
pos: `${path.basename(this.sourceFile.fileName)}:${getLineNumberString(this.sourceFile, node.getStart(this.sourceFile, false))}`,
359
private checkClassInheritance(node: ts.ClassDeclaration) {
360
const inheritance = node.heritageClauses
361
?.filter(it => it.token === ts.SyntaxKind.ExtendsKeyword)
362
if (inheritance === undefined || inheritance.length === 0) return
364
const parent = inheritance[0].types[0].expression
365
const parentDeclaration = getDeclarationsByNode(this.typeChecker, parent).find(ts.isClassDeclaration)
366
if (parentDeclaration === undefined) return
368
const nodeMethods = this.getMethodsTypes(node)
369
const parentMethods = this.getMethodsTypes(parentDeclaration)
371
nodeMethods.forEach((nodeMethod: ts.FunctionTypeNode, methodName: string) => {
372
const parentMethod = parentMethods.get(methodName)
373
if (parentMethod === undefined) return
375
if (!this.satisfies(nodeMethod, parentMethod)) {
378
LinterError.INTERFACE_METHOD_TYPE_INCONSISTENT_WITH_PARENT,
379
`${node.name!.getText()} - ${methodName}`
385
private getMethodsTypes(node: ts.ClassDeclaration): Map<string, ts.FunctionTypeNode> {
386
const map = new Map()
388
.filter(ts.isMethodDeclaration)
392
ts.factory.createFunctionTypeNode(
409
subtypeFunction: ts.FunctionTypeNode,
410
supertypeFunction: ts.FunctionTypeNode
412
if (subtypeFunction.parameters.length != supertypeFunction.parameters.length) return false
414
return zip(subtypeFunction.parameters, supertypeFunction.parameters)
415
.every(([subtypeParam, supertypeParam]) => {
416
const subtype = this.followDeclarationFunctionalType(subtypeParam.type!)
417
if (subtype === undefined) return true
418
const supertype = this.followDeclarationFunctionalType(supertypeParam.type!)
419
if (supertype === undefined) return true
421
return this.satisfies(subtype, supertype);
425
private followDeclarationFunctionalType(node: ts.TypeNode): ts.FunctionTypeNode | undefined {
426
if (ts.isFunctionTypeNode(node)) return node
427
if (!ts.isTypeReferenceNode(node)) return undefined
429
const declaredType = getDeclarationsByNode(this.typeChecker, node.typeName)
430
.find(ts.isTypeAliasDeclaration)
433
return declaredType && ts.isFunctionTypeNode(declaredType)
438
private checkOverloads(node: ts.InterfaceDeclaration | ts.ClassDeclaration) {
439
const set = new Set<string>()
441
const perMethod = (it: string) => {
445
LinterError.METHOD_OVERLOADING,
446
`Method overloaded: ${it}`
452
if (ts.isClassDeclaration(node)) {
454
.filter(ts.isMethodDeclaration)
455
.map(it => it.name.getText())
458
if (ts.isInterfaceDeclaration(node)) {
460
.filter(ts.isMethodSignature)
461
.map(it => it.name.getText())
466
private checkEmpty(node: ts.InterfaceDeclaration | ts.ClassDeclaration) {
467
if (node.heritageClauses === undefined && node.members.length === 0) {
470
LinterError.EMPTY_DECLARATION,
471
`Empty class or interface declaration ${node.name?.getText() ?? ""}`
476
private interfaceOrClassChecks(node: ts.InterfaceDeclaration | ts.ClassDeclaration) {
477
this.checkEmpty(node)
478
this.checkOverloads(node)
482
function updateHistogram(message: LinterMessage, histogram: Map<LinterError, number>): LinterMessage {
483
histogram.set(message.error, (histogram.get(message.error) ?? 0) + 1)
487
function printHistogram(histogram: Map<LinterError, number>): string {
488
let sorted = Array.from(histogram.entries()).sort((a, b) => b[1] - a[1])
489
return sorted.map(it => `${LinterError[it[0]]}: ${it[1]}`).join("\n")
492
function findEnumDFS(type: ts.TypeNode, typeChecker: ts.TypeChecker): ts.EnumDeclaration | undefined {
493
if (ts.isTypeReferenceNode(type)) {
494
for (const decl of getDeclarationsByNode(typeChecker, type.typeName)) {
495
if (ts.isEnumDeclaration(decl)) {
498
if (ts.isTypeAliasDeclaration(decl)) {
499
return findEnumDFS(decl.type, typeChecker)
506
function findEnumType(types: ts.NodeArray<ts.TypeNode>, typeChecker: ts.TypeChecker): ts.EnumDeclaration | undefined {
507
for (const type of types) {
508
const enumType = findEnumDFS(type, typeChecker)
509
if (enumType != undefined) {
516
export function toLinterString(
517
allEntries: Array<LinterMessage[]>,
518
suppressErrors: string | undefined,
519
whitelistFile: string | undefined
520
): [string, number, string] {
521
const suppressedErrorsSet = new Set<LinterError>()
522
if (suppressErrors) {
523
suppressErrors.split(",").forEach(it => suppressedErrorsSet.add(Number(it) as LinterError))
525
let whitelist: LinterWhitelist | undefined = undefined
527
whitelist = new LinterWhitelist(whitelistFile)
529
let histogram = new Map<LinterError, number>()
530
let errors = allEntries
533
.filter(it => !suppressedErrorsSet.has(it.error))
534
.filter(it => whitelist ? !whitelist.shallSuppress(it) : true)
535
.map(it => updateHistogram(it, histogram))
538
.filter(element => (element?.length ?? 0) > 0)
539
return [errors.join("\n"), errors.length > 0 ? 1 : 0, printHistogram(histogram)]