idlize

Форк
0
/
linter.ts 
540 строк · 20.7 Кб
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
    METHOD_OVERLOADING,
53
    CPP_KEYWORDS,
54
    INCORRECT_DATA_CLASS,
55
    EMPTY_DECLARATION,
56
    UNION_CONTAINS_ENUM,
57
}
58

59
export interface LinterMessage {
60
    file: ts.SourceFile
61
    pos: string
62
    message: string,
63
    error: LinterError
64
    node: ts.Node
65
}
66

67
const suppressed = new Set([LinterError.UNION_CONTAINS_ENUM])
68

69
function stringMessage(message: LinterMessage): string {
70
    return `${message.pos} - [${LinterError[message.error]}] ${message.message}`
71
}
72

73
let allInterfaces = new Map<string, string>()
74

75
export class LinterVisitor implements GenericVisitor<LinterMessage[]> {
76
    private output: LinterMessage[] = []
77

78
    constructor(private sourceFile: ts.SourceFile, private typeChecker: ts.TypeChecker) {
79
    }
80

81
    visitWholeFile(): LinterMessage[] {
82
        ts.forEachChild(this.sourceFile, (node) => this.visit(node))
83
        return this.output
84
    }
85

86
    visit(node: ts.Node): void {
87
        if (ts.isClassDeclaration(node)) {
88
            this.visitClass(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)) {
94
            this.visitEnum(node)
95
        } else if (ts.isFunctionDeclaration(node)) {
96
            this.visitFunctionDeclaration(node)
97
        } else if (ts.isTypeAliasDeclaration(node)) {
98
            this.visitTypeAlias(node)
99
        }
100
    }
101

102
    visitNamespace(node: ts.ModuleDeclaration) {
103
        if (node.name) {
104
            this.report(node, LinterError.NAMESPACE, `Namespace detected: ${asString(node.name)}`)
105
        }
106
        ts.forEachChild(node, this.visit)
107
    }
108

109
    visitClass(clazz: ts.ClassDeclaration): void {
110
        this.checkClassDuplicate(clazz)
111
        const allInheritCount = clazz.heritageClauses
112
            ?.map(it => it.types)
113
            ?.flatMap(it => it)
114
            ?.length ?? 0
115
        if (allInheritCount > 1) {
116
            this.report(clazz, LinterError.MULTIPLE_INHERITANCE, `Multiple inheritance for class ${asString(clazz.name)}`)
117
        }
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`)
120
        }
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)
128
            }
129
        })
130
        this.checkClassInheritance(clazz)
131
        this.interfaceOrClassChecks(clazz)
132
    }
133

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)}`)
139
        }
140
        allInterfaces.set(clazzName, clazz.getSourceFile().fileName)
141
    }
142

143
    visitInterface(clazz: ts.InterfaceDeclaration): void {
144
        this.checkClassDuplicate(clazz)
145
        const allInheritCount = clazz.heritageClauses
146
            ?.map(it => it.types)
147
            ?.flatMap(it => it)
148
            ?.length ?? 0
149
        if (allInheritCount > 1) {
150
            this.report(clazz, LinterError.MULTIPLE_INHERITANCE, `Multiple inheritance for interface ${asString(clazz.name)}`)
151
        }
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)}`)
155
            }
156
        })
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)
166
            }
167
        })
168
        this.interfaceOrClassChecks(clazz)
169
    }
170

171
    checkType(type: ts.TypeNode | undefined): void {
172
        if (!type) return
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()}`)
176
            return
177
        }
178
        if (ts.isArrayTypeNode(type)) {
179
            this.checkType(type.elementType)
180
            return
181
        }
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))
189
                }
190

191
            })
192
        }
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}'`)
197
            }
198
            type.types.forEach(it => {
199
                this.checkType(it)
200
            })
201
        }
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`)
208
                }
209
            }
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)}`)
213
            }
214
        }
215
        if (ts.isParenthesizedTypeNode(type)) {
216
            this.checkType(type.type)
217
        }
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))
221
        }
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)
226
        }
227
        if (ts.isTemplateLiteralTypeNode(type)) {
228
            this.report(type, LinterError.TEMPLATE_LITERAL, `Template literal: ${type.getText(this.sourceFile)}`)
229
        }
230
        if (ts.isImportTypeNode(type)) {
231
            this.report(type, LinterError.IMPORT_TYPE, `Import type: ${type.getText(this.sourceFile)}`)
232
        }
233
        if (this.isTypeParameterReferenceAndNotCommonMethod(type)) {
234
            this.report(type, LinterError.UNSUPPORTED_TYPE_PARAMETER, `Unsupported type parameter: ${type.getText(this.sourceFile)}`)
235
        }
236
    }
237

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)
247
            }
248
        }
249
        return false
250
    }
251

252
    checkName(name: ts.PropertyName | undefined): void {
253
        if (!name) return
254
        if (ts.isComputedPropertyName(name)) {
255
            this.report(name, LinterError.COMPUTED_PROPERTY_NAME, `Computed property name ${name.getText(this.sourceFile)}`)
256
        } else {
257
            let nameString = identName(name)!
258
            if (LinterVisitor.cppKeywords.has(nameString)) {
259
                this.report(
260
                    name,
261
                    LinterError.CPP_KEYWORDS,
262
                    `Use C/C++ keyword as the field name: ${nameString}`
263
                )
264
            }
265
        }
266
    }
267

268
    // See https://en.cppreference.com/w/cpp/keyword.
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`,
289
        `xor_eq`
290
    ])
291

292
    visitConstructor(ctor: ts.ConstructorDeclaration | ts.ConstructSignatureDeclaration): void {
293
        ctor.parameters.map(param => this.checkType(param.type))
294
    }
295

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)}`)
301
            }
302
        })
303
        method.parameters.forEach(it => this.visitParameter(it))
304
    }
305

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")
310
        }
311
        this.inParamCheck = true
312
        this.checkType(parameter.type)
313
        this.inParamCheck = false
314

315
    }
316

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)}`)
321
            }
322
        })
323
        this.checkType(property.type)
324
        this.checkName(property.name)
325
    }
326

327
    visitEnum(enumDeclaration: ts.EnumDeclaration): void {
328
        enumDeclaration.members.forEach(member => {
329
            if (member.initializer && !ts.isNumericLiteral(member.initializer)) {
330
                this.report(
331
                    member,
332
                    LinterError.ENUM_WITH_INIT,
333
                    `Enum ${nameOrNullForIdl(enumDeclaration.name)}.${nameOrNullForIdl(member.name)} with non-int initializer: ${member.initializer.getText(this.sourceFile)}`
334
                )
335
            }
336
        })
337
    }
338

339
    visitTypeAlias(type: ts.TypeAliasDeclaration): void {
340
        this.checkType(type.type)
341
    }
342

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))
346
    }
347

348
    report(node: ts.Node, error: LinterError, message: string): void {
349
        if (suppressed.has(error)) return
350
        this.output.push({
351
            file: this.sourceFile,
352
            pos: `${path.basename(this.sourceFile.fileName)}:${getLineNumberString(this.sourceFile, node.getStart(this.sourceFile, false))}`,
353
            message: message,
354
            error: error,
355
            node: node
356
        })
357
    }
358

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
363

364
        const parent = inheritance[0].types[0].expression
365
        const parentDeclaration = getDeclarationsByNode(this.typeChecker, parent).find(ts.isClassDeclaration)
366
        if (parentDeclaration === undefined) return
367

368
        const nodeMethods = this.getMethodsTypes(node)
369
        const parentMethods = this.getMethodsTypes(parentDeclaration)
370

371
        nodeMethods.forEach((nodeMethod: ts.FunctionTypeNode, methodName: string) => {
372
            const parentMethod = parentMethods.get(methodName)
373
            if (parentMethod === undefined) return
374

375
            if (!this.satisfies(nodeMethod, parentMethod)) {
376
                this.report(
377
                    node,
378
                    LinterError.INTERFACE_METHOD_TYPE_INCONSISTENT_WITH_PARENT,
379
                    `${node.name!.getText()} - ${methodName}`
380
                )
381
            }
382
        })
383
    }
384

385
    private getMethodsTypes(node: ts.ClassDeclaration): Map<string, ts.FunctionTypeNode> {
386
        const map = new Map()
387
        node.members
388
            .filter(ts.isMethodDeclaration)
389
            .forEach(it =>
390
                map.set(
391
                    it.name.getText(),
392
                    ts.factory.createFunctionTypeNode(
393
                        undefined,
394
                        it.parameters,
395
                        it.type!
396
                    )
397
                )
398
            )
399
        return map
400
    }
401

402
    /*
403
        ts.typechecker doesn't provide the functionality to check if type1 satisfies type2,
404
        so we'll implement it for functional types, by comparing recursively number of parameters
405

406
        currently, this one's needed only to report ScrollAttribute
407
     */
408
    private satisfies(
409
        subtypeFunction: ts.FunctionTypeNode,
410
        supertypeFunction: ts.FunctionTypeNode
411
    ): boolean {
412
        if (subtypeFunction.parameters.length != supertypeFunction.parameters.length) return false
413

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
420

421
                return this.satisfies(subtype, supertype);
422
            })
423
    }
424

425
    private followDeclarationFunctionalType(node: ts.TypeNode): ts.FunctionTypeNode | undefined {
426
        if (ts.isFunctionTypeNode(node)) return node
427
        if (!ts.isTypeReferenceNode(node)) return undefined
428

429
        const declaredType = getDeclarationsByNode(this.typeChecker, node.typeName)
430
            .find(ts.isTypeAliasDeclaration)
431
            ?.type
432

433
        return declaredType && ts.isFunctionTypeNode(declaredType)
434
            ? declaredType
435
            : undefined
436
    }
437

438
    private checkOverloads(node: ts.InterfaceDeclaration | ts.ClassDeclaration) {
439
        const set = new Set<string>()
440

441
        const perMethod = (it: string) => {
442
            if (set.has(it)) {
443
                this.report(
444
                    node,
445
                    LinterError.METHOD_OVERLOADING,
446
                    `Method overloaded: ${it}`
447
                )
448
            }
449
            set.add(it)
450
        }
451

452
        if (ts.isClassDeclaration(node)) {
453
            node.members
454
                .filter(ts.isMethodDeclaration)
455
                .map(it => it.name.getText())
456
                .forEach(perMethod)
457
        }
458
        if (ts.isInterfaceDeclaration(node)) {
459
            node.members
460
                .filter(ts.isMethodSignature)
461
                .map(it => it.name.getText())
462
                .forEach(perMethod)
463
        }
464
    }
465

466
    private checkEmpty(node: ts.InterfaceDeclaration | ts.ClassDeclaration) {
467
        if (node.heritageClauses === undefined && node.members.length === 0) {
468
            this.report(
469
                node,
470
                LinterError.EMPTY_DECLARATION,
471
                `Empty class or interface declaration ${node.name?.getText() ?? ""}`
472
            )
473
        }
474
    }
475

476
    private interfaceOrClassChecks(node: ts.InterfaceDeclaration | ts.ClassDeclaration) {
477
        this.checkEmpty(node)
478
        this.checkOverloads(node)
479
    }
480
}
481

482
function updateHistogram(message: LinterMessage, histogram: Map<LinterError, number>): LinterMessage {
483
    histogram.set(message.error, (histogram.get(message.error) ?? 0) + 1)
484
    return message
485
}
486

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")
490
}
491

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)) {
496
                return decl
497
            }
498
            if (ts.isTypeAliasDeclaration(decl)) {
499
                return findEnumDFS(decl.type, typeChecker)
500
            }
501
        }
502
    }
503
    return undefined
504
}
505

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) {
510
            return enumType
511
        }
512
    }
513
    return undefined
514
}
515

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))
524
    }
525
    let whitelist: LinterWhitelist | undefined = undefined
526
    if (whitelistFile) {
527
        whitelist = new LinterWhitelist(whitelistFile)
528
    }
529
    let histogram = new Map<LinterError, number>()
530
    let errors = allEntries
531
        .flatMap(entries =>
532
            entries
533
                .filter(it => !suppressedErrorsSet.has(it.error))
534
                .filter(it => whitelist ? !whitelist.shallSuppress(it) : true)
535
                .map(it => updateHistogram(it, histogram))
536
                .map(stringMessage)
537
        )
538
        .filter(element => (element?.length ?? 0) > 0)
539
    return [errors.join("\n"), errors.length > 0 ? 1 : 0, printHistogram(histogram)]
540
}
541

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

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

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

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