2
* Copyright (c) 2024 Huawei Device Co., Ltd.
3
* Licensed under the Apache License, Version 2.0 (the "License");
4
* you may not use this file except in compliance with the License.
5
* You may obtain a copy of the License at
7
* http://www.apache.org/licenses/LICENSE-2.0
9
* Unless required by applicable law or agreed to in writing, software
10
* distributed under the License is distributed on an "AS IS" BASIS,
11
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
* See the License for the specific language governing permissions and
13
* limitations under the License.
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,
54
export interface LinterMessage {
62
function stringMessage(message: LinterMessage): string {
63
return `${message.pos} - [${LinterError[message.error]}] ${message.message}`
66
let allInterfaces = new Map<string, string>()
68
export class LinterVisitor implements GenericVisitor<LinterMessage[]> {
69
private output: LinterMessage[] = []
71
constructor(private sourceFile: ts.SourceFile, private typeChecker: ts.TypeChecker) {
74
visitWholeFile(): LinterMessage[] {
75
ts.forEachChild(this.sourceFile, (node) => this.visit(node))
79
visit(node: ts.Node): void {
80
if (ts.isClassDeclaration(node)) {
82
} else if (ts.isInterfaceDeclaration(node)) {
83
this.visitInterface(node)
84
} else if (ts.isModuleDeclaration(node)) {
85
this.visitNamespace(node)
86
} else if (ts.isEnumDeclaration(node)) {
88
} else if (ts.isFunctionDeclaration(node)) {
89
this.visitFunctionDeclaration(node)
90
} else if (ts.isTypeAliasDeclaration(node)) {
91
this.visitTypeAlias(node)
95
visitNamespace(node: ts.ModuleDeclaration) {
97
this.report(node, LinterError.NAMESPACE, `Namespace detected: ${asString(node.name)}`)
99
ts.forEachChild(node, this.visit)
102
visitClass(clazz: ts.ClassDeclaration): void {
103
this.checkClassDuplicate(clazz)
104
const allInheritCount = clazz.heritageClauses
105
?.map(it => it.types)
108
if (allInheritCount > 1) {
109
this.report(clazz, LinterError.MULTIPLE_INHERITANCE, `Multiple inheritance for class ${asString(clazz.name)}`)
111
clazz.members.forEach(child => {
112
if (ts.isConstructorDeclaration(child)) {
113
this.visitConstructor(child)
114
} else if (ts.isMethodDeclaration(child)) {
115
this.visitMethod(child)
116
} else if (ts.isPropertyDeclaration(child)) {
117
this.visitProperty(child)
120
this.checkClassInheritance(clazz)
123
checkClassDuplicate(clazz: ts.InterfaceDeclaration | ts.ClassDeclaration) {
124
let clazzName = asString(clazz.name)
125
if (allInterfaces.has(clazzName)) {
126
this.report(clazz, LinterError.DUPLICATE_INTERFACE,
127
`Duplicate interface ${clazzName}: ${clazz.getSourceFile().fileName} and ${allInterfaces.get(clazzName)}`)
129
allInterfaces.set(clazzName, clazz.getSourceFile().fileName)
132
visitInterface(clazz: ts.InterfaceDeclaration): void {
133
this.checkClassDuplicate(clazz)
134
const allInheritCount = clazz.heritageClauses
135
?.map(it => it.types)
138
if (allInheritCount > 1) {
139
this.report(clazz, LinterError.MULTIPLE_INHERITANCE, `Multiple inheritance for interface ${asString(clazz.name)}`)
141
clazz.modifiers?.forEach(it => {
142
if (it.kind == ts.SyntaxKind.PrivateKeyword) {
143
this.report(clazz, LinterError.PRIVATE_VISIBILITY, `Private visibility is useless: ${clazz.getText(this.sourceFile).substring(0, 50)}`)
146
clazz.members.forEach(child => {
147
if (ts.isConstructSignatureDeclaration(child)) {
148
this.visitConstructor(child)
149
} else if (ts.isMethodSignature(child)) {
150
this.visitMethod(child)
151
} else if (ts.isPropertySignature(child)) {
152
this.visitProperty(child)
157
checkType(type: ts.TypeNode | undefined): void {
159
if (type.kind == ts.SyntaxKind.AnyKeyword) {
160
let parent = type.parent
161
this.report(type, LinterError.ANY_KEYWORD, `Keyword "any" is disalowed: ${parent.getText(parent.getSourceFile())}`)
164
if (ts.isArrayTypeNode(type)) {
165
this.checkType(type.elementType)
168
if (ts.isTypeLiteralNode(type)) {
169
this.report(type, LinterError.TYPE_LITERAL, `Type literal`)
170
type.members.forEach(it => {
171
if (ts.isPropertySignature(it)) this.visitProperty(it)
172
if (ts.isIndexSignatureDeclaration(it)) {
173
this.report(it, LinterError.INDEX_SIGNATURE, `Index signature type: ${type.getText(this.sourceFile)}`)
174
it.parameters.forEach(it => this.checkType(it.type))
179
if (ts.isUnionTypeNode(type)) {
180
type.types.forEach(it => {
184
if (ts.isTypeReferenceNode(type)) {
185
if (this.inParamCheck) {
186
const declarations = getDeclarationsByNode(this.typeChecker, type.typeName)
187
if (declarations.length > 0 && ts.isClassDeclaration(declarations[0])
188
&& isCommonMethodOrSubclass(this.typeChecker, declarations[0])) {
189
this.report(type, LinterError.USE_COMPONENT_AS_PARAM, `Component ${identName(declarations[0].name)} used as parameter`)
192
if (ts.isQualifiedName(type.typeName)) {
193
this.report(type, LinterError.TYPE_ELEMENT_TYPE,
194
`Type element types unsupported, use type "${ts.idText(type.typeName.left as ts.Identifier)}" itself: ${type.getText(this.sourceFile)}`)
197
if (ts.isParenthesizedTypeNode(type)) {
198
this.checkType(type.type)
200
if (ts.isTupleTypeNode(type)) {
201
this.report(type, LinterError.TUPLE_TYPE, `Tuple type: ${type.getText(this.sourceFile)}`)
202
type.elements.forEach(it => this.checkType(it))
204
if (ts.isIndexedAccessTypeNode(type)) {
205
this.report(type, LinterError.INDEXED_ACCESS_TYPE, `Indexed access type: ${type.getText(this.sourceFile)}`)
206
this.checkType(type.indexType)
207
this.checkType(type.objectType)
209
if (ts.isTemplateLiteralTypeNode(type)) {
210
this.report(type, LinterError.TEMPLATE_LITERAL, `Template literal: ${type.getText(this.sourceFile)}`)
212
if (ts.isImportTypeNode(type)) {
213
this.report(type, LinterError.IMPORT_TYPE, `Import type: ${type.getText(this.sourceFile)}`)
215
if (this.isTypeParameterReferenceAndNotCommonMethod(type)) {
216
this.report(type, LinterError.UNSUPPORTED_TYPE_PARAMETER, `Unsupported type parameter: ${type.getText(this.sourceFile)}`)
220
isTypeParameterReferenceAndNotCommonMethod(type: ts.TypeNode): boolean {
221
if (!ts.isTypeReferenceNode(type)) return false
222
const name = type.typeName
223
const declaration = getDeclarationsByNode(this.typeChecker, name)[0]
224
if (!declaration) return false
225
if (ts.isTypeParameterDeclaration(declaration)) {
226
let parent = declaration.parent
227
if (ts.isClassDeclaration(parent)) {
228
return isCommonMethodOrSubclass(this.typeChecker, parent)
234
checkName(name: ts.PropertyName | undefined): void {
236
if (ts.isComputedPropertyName(name)) {
237
this.report(name, LinterError.COMPUTED_PROPERTY_NAME, `Computed property name ${name.getText(this.sourceFile)}`)
241
visitConstructor(ctor: ts.ConstructorDeclaration | ts.ConstructSignatureDeclaration): void {
242
ctor.parameters.map(param => this.checkType(param.type))
245
visitMethod(method: ts.MethodDeclaration | ts.MethodSignature): void {
246
this.checkType(method.type)
247
method.modifiers?.forEach(it => {
248
if (it.kind == ts.SyntaxKind.PrivateKeyword) {
249
this.report(method, LinterError.PRIVATE_VISIBILITY, `Private visibility is useless: Private visibility is useless: ${method.getText(this.sourceFile).substring(0, 50)}`)
252
method.parameters.forEach(it => this.visitParameter(it))
255
private inParamCheck = false
256
visitParameter(parameter: ts.ParameterDeclaration): void {
257
if (parameter.initializer) {
258
this.report(parameter, LinterError.PARAMETER_INITIALIZER, "Parameter initializer is forbidden")
260
this.inParamCheck = true
261
this.checkType(parameter.type)
262
this.inParamCheck = false
266
visitProperty(property: ts.PropertySignature | ts.PropertyDeclaration): void {
267
property.modifiers?.forEach(it => {
268
if (it.kind == ts.SyntaxKind.PrivateKeyword) {
269
this.report(property, LinterError.PRIVATE_VISIBILITY, `Private visibility is useless: ${property.getText(this.sourceFile)}`)
272
this.checkType(property.type)
273
this.checkName(property.name)
276
visitEnum(enumDeclaration: ts.EnumDeclaration): void {
277
enumDeclaration.members.forEach(member => {
278
if (member.initializer && !ts.isNumericLiteral(member.initializer)) {
281
LinterError.ENUM_WITH_INIT,
282
`Enum ${nameOrNullForIdl(enumDeclaration.name)}.${nameOrNullForIdl(member.name)} with non-int initializer: ${member.initializer.getText(this.sourceFile)}`
288
visitTypeAlias(type: ts.TypeAliasDeclaration): void {
289
this.checkType(type.type)
292
visitFunctionDeclaration(functionDeclaration: ts.FunctionDeclaration): void {
293
this.report(functionDeclaration, LinterError.TOP_LEVEL_FUNCTIONS, `Top level function: ${functionDeclaration.getText(this.sourceFile)}`)
294
functionDeclaration.parameters.forEach(it => this.visitParameter(it))
297
report(node: ts.Node, error: LinterError, message: string): void {
299
file: this.sourceFile,
300
pos: `${path.basename(this.sourceFile.fileName)}:${getLineNumberString(this.sourceFile, node.getStart(this.sourceFile, false))}`,
307
private checkClassInheritance(node: ts.ClassDeclaration) {
308
const inheritance = node.heritageClauses
309
?.filter(it => it.token === ts.SyntaxKind.ExtendsKeyword)
310
if (inheritance === undefined || inheritance.length === 0) return
312
const parent = inheritance[0].types[0].expression
313
const parentDeclaration = getDeclarationsByNode(this.typeChecker, parent).find(ts.isClassDeclaration)
314
if (parentDeclaration === undefined) return
316
const nodeMethods = this.getMethodsTypes(node)
317
const parentMethods = this.getMethodsTypes(parentDeclaration)
319
nodeMethods.forEach((nodeMethod: ts.FunctionTypeNode, methodName: string) => {
320
const parentMethod = parentMethods.get(methodName)
321
if (parentMethod === undefined) return
323
if (!this.satisfies(nodeMethod, parentMethod)) {
326
LinterError.INTERFACE_METHOD_TYPE_INCONSISTENT_WITH_PARENT,
327
`${node.name!.getText()} - ${methodName}`
333
private getMethodsTypes(node: ts.ClassDeclaration): Map<string, ts.FunctionTypeNode> {
334
const map = new Map()
336
.filter(ts.isMethodDeclaration)
340
ts.factory.createFunctionTypeNode(
351
ts.typechecker doesn't provide the functionality to check if type1 satisfies type2,
352
so we'll implement it for functional types, by comparing recursively number of parameters
354
currently, this one's needed only to report ScrollAttribute
357
subtypeFunction: ts.FunctionTypeNode,
358
supertypeFunction: ts.FunctionTypeNode
360
if (subtypeFunction.parameters.length != supertypeFunction.parameters.length) return false
362
return zip(subtypeFunction.parameters, supertypeFunction.parameters)
363
.every(([subtypeParam, supertypeParam]) => {
364
const subtype = this.followDeclarationFunctionalType(subtypeParam.type!)
365
if (subtype === undefined) return true
366
const supertype = this.followDeclarationFunctionalType(supertypeParam.type!)
367
if (supertype === undefined) return true
369
return this.satisfies(subtype, supertype);
373
private followDeclarationFunctionalType(node: ts.TypeNode): ts.FunctionTypeNode | undefined {
374
if (ts.isFunctionTypeNode(node)) return node
375
if (!ts.isTypeReferenceNode(node)) return undefined
377
const declaredType = getDeclarationsByNode(this.typeChecker, node.typeName)
378
.find(ts.isTypeAliasDeclaration)
381
return declaredType && ts.isFunctionTypeNode(declaredType)
387
function updateHistorgam(message: LinterMessage, histogram: Map<LinterError, number>): LinterMessage {
388
histogram.set(message.error, (histogram.get(message.error) ?? 0) + 1)
392
function printHistorgam(histogram: Map<LinterError, number>): string {
393
let sorted = Array.from(histogram.entries()).sort((a, b) => b[1] - a[1])
394
return sorted.map(it => `${LinterError[it[0]]}: ${it[1]}`).join("\n")
397
export function toLinterString(
398
allEntries: Array<LinterMessage[]>,
399
suppressErrors: string | undefined,
400
whitelistFile: string | undefined
401
): [string, number, string] {
402
const suppressedErrorsSet = new Set<LinterError>()
403
if (suppressErrors) {
404
suppressErrors.split(",").forEach(it => suppressedErrorsSet.add(Number(it) as LinterError))
406
let whitelist: LinterWhitelist| undefined = undefined
408
whitelist = new LinterWhitelist(whitelistFile)
410
let histogram = new Map<LinterError, number>()
411
let errors = allEntries
414
.filter(it => !suppressedErrorsSet.has(it.error))
415
.filter(it => whitelist ? !whitelist.shallSuppress(it) : true)
416
.map(it => updateHistorgam(it, histogram))
419
.filter(element => (element?.length ?? 0) > 0)
420
return [errors.join("\n"), errors.length > 0 ? 1 : 0, printHistorgam(histogram)]