idlize

Форк
0
/
linter.ts 
421 строка · 16.1 Кб
1
/*
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
6
 *
7
 * http://www.apache.org/licenses/LICENSE-2.0
8
 *
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.
14
 */
15

16
import * as ts from "typescript"
17
import { GenericVisitor } from "./options"
18
import * as path from "path"
19
import {
20
    asString,
21
    getDeclarationsByNode,
22
    getLineNumberString,
23
    identName,
24
    isCommonMethodOrSubclass,
25
    nameOrNullForIdl,
26
    zip
27
} from "./util"
28
import { LinterWhitelist } from "./LinterWhitelist"
29

30
export enum LinterError {
31
    NONE,
32
    TYPE_LITERAL,
33
    ENUM_WITH_INIT,
34
    COMPUTED_PROPERTY_NAME,
35
    TUPLE_TYPE,
36
    INDEXED_ACCESS_TYPE,
37
    TEMPLATE_LITERAL,
38
    IMPORT_TYPE,
39
    MULTIPLE_INHERITANCE,
40
    UNSUPPORTED_TYPE_PARAMETER,
41
    PARAMETER_INITIALIZER,
42
    DUPLICATE_INTERFACE,
43
    INDEX_SIGNATURE,
44
    NAMESPACE,
45
    NUMBER_TYPE,
46
    PRIVATE_VISIBILITY,
47
    TOP_LEVEL_FUNCTIONS,
48
    ANY_KEYWORD,
49
    TYPE_ELEMENT_TYPE,
50
    INTERFACE_METHOD_TYPE_INCONSISTENT_WITH_PARENT,
51
    USE_COMPONENT_AS_PARAM,
52
}
53

54
export interface LinterMessage {
55
    file: ts.SourceFile
56
    pos: string
57
    message: string,
58
    error: LinterError
59
    node: ts.Node
60
}
61

62
function stringMessage(message: LinterMessage): string {
63
    return `${message.pos} - [${LinterError[message.error]}] ${message.message}`
64
}
65

66
let allInterfaces = new Map<string, string>()
67

68
export class LinterVisitor implements GenericVisitor<LinterMessage[]> {
69
    private output: LinterMessage[] = []
70

71
    constructor(private sourceFile: ts.SourceFile, private typeChecker: ts.TypeChecker) {
72
    }
73

74
    visitWholeFile(): LinterMessage[] {
75
        ts.forEachChild(this.sourceFile, (node) => this.visit(node))
76
        return this.output
77
    }
78

79
    visit(node: ts.Node): void {
80
        if (ts.isClassDeclaration(node)) {
81
            this.visitClass(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)) {
87
            this.visitEnum(node)
88
        } else if (ts.isFunctionDeclaration(node)) {
89
            this.visitFunctionDeclaration(node)
90
        } else if (ts.isTypeAliasDeclaration(node)) {
91
            this.visitTypeAlias(node)
92
        }
93
    }
94

95
    visitNamespace(node: ts.ModuleDeclaration) {
96
        if (node.name) {
97
            this.report(node, LinterError.NAMESPACE, `Namespace detected: ${asString(node.name)}`)
98
        }
99
        ts.forEachChild(node, this.visit)
100
    }
101

102
    visitClass(clazz: ts.ClassDeclaration): void {
103
        this.checkClassDuplicate(clazz)
104
        const allInheritCount = clazz.heritageClauses
105
            ?.map(it => it.types)
106
            ?.flatMap(it => it)
107
            ?.length ?? 0
108
        if (allInheritCount > 1) {
109
            this.report(clazz, LinterError.MULTIPLE_INHERITANCE, `Multiple inheritance for class ${asString(clazz.name)}`)
110
        }
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)
118
            }
119
        })
120
        this.checkClassInheritance(clazz)
121
    }
122

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)}`)
128
        }
129
        allInterfaces.set(clazzName, clazz.getSourceFile().fileName)
130
    }
131

132
    visitInterface(clazz: ts.InterfaceDeclaration): void {
133
        this.checkClassDuplicate(clazz)
134
        const allInheritCount = clazz.heritageClauses
135
            ?.map(it => it.types)
136
            ?.flatMap(it => it)
137
            ?.length ?? 0
138
        if (allInheritCount > 1) {
139
            this.report(clazz, LinterError.MULTIPLE_INHERITANCE, `Multiple inheritance for interface ${asString(clazz.name)}`)
140
        }
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)}`)
144
            }
145
        })
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)
153
            }
154
        })
155
    }
156

157
    checkType(type: ts.TypeNode | undefined): void {
158
        if (!type) return
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())}`)
162
            return
163
        }
164
        if (ts.isArrayTypeNode(type)) {
165
            this.checkType(type.elementType)
166
            return
167
        }
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))
175
                }
176

177
            })
178
        }
179
        if (ts.isUnionTypeNode(type)) {
180
            type.types.forEach(it => {
181
                this.checkType(it)
182
            })
183
        }
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`)
190
                }
191
            }
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)}`)
195
            }
196
        }
197
        if (ts.isParenthesizedTypeNode(type)) {
198
            this.checkType(type.type)
199
        }
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))
203
        }
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)
208
        }
209
        if (ts.isTemplateLiteralTypeNode(type)) {
210
            this.report(type, LinterError.TEMPLATE_LITERAL, `Template literal: ${type.getText(this.sourceFile)}`)
211
        }
212
        if (ts.isImportTypeNode(type)) {
213
            this.report(type, LinterError.IMPORT_TYPE, `Import type: ${type.getText(this.sourceFile)}`)
214
        }
215
        if (this.isTypeParameterReferenceAndNotCommonMethod(type)) {
216
            this.report(type, LinterError.UNSUPPORTED_TYPE_PARAMETER, `Unsupported type parameter: ${type.getText(this.sourceFile)}`)
217
        }
218
    }
219

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)
229
            }
230
        }
231
        return false
232
    }
233

234
    checkName(name: ts.PropertyName | undefined): void {
235
        if (!name) return
236
        if (ts.isComputedPropertyName(name)) {
237
            this.report(name, LinterError.COMPUTED_PROPERTY_NAME, `Computed property name ${name.getText(this.sourceFile)}`)
238
        }
239
    }
240

241
    visitConstructor(ctor: ts.ConstructorDeclaration | ts.ConstructSignatureDeclaration): void {
242
        ctor.parameters.map(param => this.checkType(param.type))
243
    }
244

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)}`)
250
            }
251
        })
252
        method.parameters.forEach(it => this.visitParameter(it))
253
    }
254

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")
259
        }
260
        this.inParamCheck = true
261
        this.checkType(parameter.type)
262
        this.inParamCheck = false
263

264
    }
265

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)}`)
270
            }
271
        })
272
        this.checkType(property.type)
273
        this.checkName(property.name)
274
    }
275

276
    visitEnum(enumDeclaration: ts.EnumDeclaration): void {
277
        enumDeclaration.members.forEach(member => {
278
            if (member.initializer && !ts.isNumericLiteral(member.initializer)) {
279
                this.report(
280
                    member,
281
                    LinterError.ENUM_WITH_INIT,
282
                    `Enum ${nameOrNullForIdl(enumDeclaration.name)}.${nameOrNullForIdl(member.name)} with non-int initializer: ${member.initializer.getText(this.sourceFile)}`
283
                )
284
            }
285
        })
286
    }
287

288
    visitTypeAlias(type: ts.TypeAliasDeclaration): void {
289
        this.checkType(type.type)
290
    }
291

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))
295
    }
296

297
    report(node: ts.Node, error: LinterError, message: string): void {
298
        this.output.push({
299
            file: this.sourceFile,
300
            pos: `${path.basename(this.sourceFile.fileName)}:${getLineNumberString(this.sourceFile, node.getStart(this.sourceFile, false))}`,
301
            message: message,
302
            error: error,
303
            node: node
304
        })
305
    }
306

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
311

312
        const parent = inheritance[0].types[0].expression
313
        const parentDeclaration = getDeclarationsByNode(this.typeChecker, parent).find(ts.isClassDeclaration)
314
        if (parentDeclaration === undefined) return
315

316
        const nodeMethods = this.getMethodsTypes(node)
317
        const parentMethods = this.getMethodsTypes(parentDeclaration)
318

319
        nodeMethods.forEach((nodeMethod: ts.FunctionTypeNode, methodName: string) => {
320
            const parentMethod = parentMethods.get(methodName)
321
            if (parentMethod === undefined) return
322

323
            if (!this.satisfies(nodeMethod, parentMethod)) {
324
                this.report(
325
                    node,
326
                    LinterError.INTERFACE_METHOD_TYPE_INCONSISTENT_WITH_PARENT,
327
                    `${node.name!.getText()} - ${methodName}`
328
                )
329
            }
330
        })
331
    }
332

333
    private getMethodsTypes(node: ts.ClassDeclaration): Map<string, ts.FunctionTypeNode> {
334
        const map = new Map()
335
        node.members
336
            .filter(ts.isMethodDeclaration)
337
            .forEach(it =>
338
                map.set(
339
                    it.name.getText(),
340
                    ts.factory.createFunctionTypeNode(
341
                        undefined,
342
                        it.parameters,
343
                        it.type!
344
                    )
345
                )
346
            )
347
        return map
348
    }
349

350
    /*
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
353

354
        currently, this one's needed only to report ScrollAttribute
355
     */
356
    private satisfies(
357
        subtypeFunction: ts.FunctionTypeNode,
358
        supertypeFunction: ts.FunctionTypeNode
359
    ): boolean {
360
        if (subtypeFunction.parameters.length != supertypeFunction.parameters.length) return false
361

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
368

369
                return this.satisfies(subtype, supertype);
370
            })
371
    }
372

373
    private followDeclarationFunctionalType(node: ts.TypeNode): ts.FunctionTypeNode | undefined {
374
        if (ts.isFunctionTypeNode(node)) return node
375
        if (!ts.isTypeReferenceNode(node)) return undefined
376

377
        const declaredType = getDeclarationsByNode(this.typeChecker, node.typeName)
378
            .find(ts.isTypeAliasDeclaration)
379
            ?.type
380

381
        return declaredType && ts.isFunctionTypeNode(declaredType)
382
            ? declaredType
383
            : undefined
384
    }
385
}
386

387
function updateHistorgam(message: LinterMessage, histogram: Map<LinterError, number>): LinterMessage {
388
    histogram.set(message.error, (histogram.get(message.error) ?? 0) + 1)
389
    return message
390
}
391

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")
395
}
396

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))
405
    }
406
    let whitelist: LinterWhitelist| undefined = undefined
407
    if (whitelistFile) {
408
        whitelist = new LinterWhitelist(whitelistFile)
409
    }
410
    let histogram = new Map<LinterError, number>()
411
    let errors = allEntries
412
        .flatMap(entries =>
413
            entries
414
                .filter(it => !suppressedErrorsSet.has(it.error))
415
                .filter(it => whitelist ? !whitelist.shallSuppress(it) : true)
416
                .map(it => updateHistorgam(it, histogram))
417
                .map(stringMessage)
418
        )
419
        .filter(element => (element?.length ?? 0) > 0)
420
    return [errors.join("\n"), errors.length > 0 ? 1 : 0,  printHistorgam(histogram)]
421
}
422

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.