1/* 2 * Copyright (c) 2025 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 16import type * as ts from 'typescript'; 17import type { LinterOptions } from './LinterOptions'; 18import type { ProblemInfo } from './ProblemInfo'; 19import type { Autofix } from './autofixes/Autofixer'; 20import { FileStatistics } from './statistics/FileStatistics'; 21import { TsUtils } from './utils/TsUtils'; 22import { cookBookRefToFixTitle } from './autofixes/AutofixTitles'; 23import { faultDesc } from './FaultDesc'; 24import { TypeScriptLinterConfig } from './TypeScriptLinterConfig'; 25import { faultsAttrs } from './FaultAttrs'; 26import { cookBookTag } from './CookBookMsg'; 27import { FaultID } from './Problems'; 28import { ProblemSeverity } from './ProblemSeverity'; 29import { arkts2Rules, onlyArkts2SyntaxRules } from './utils/consts/ArkTS2Rules'; 30 31export abstract class BaseTypeScriptLinter { 32 problemsInfos: ProblemInfo[] = []; 33 fileStats: FileStatistics; 34 tsUtils: TsUtils; 35 36 constructor( 37 protected readonly tsTypeChecker: ts.TypeChecker, 38 readonly options: LinterOptions, 39 protected sourceFile: ts.SourceFile 40 ) { 41 this.tsUtils = new TsUtils(this.tsTypeChecker, options); 42 this.fileStats = new FileStatistics(sourceFile, this.problemsInfos); 43 } 44 45 protected getLineAndCharacterOfNode(node: ts.Node | ts.CommentRange): ts.LineAndCharacter { 46 const startPos = TsUtils.getStartPos(node); 47 const { line, character } = this.sourceFile.getLineAndCharacterOfPosition(startPos); 48 // TSC counts lines and columns from zero 49 return { line: line + 1, character: character + 1 }; 50 } 51 52 abstract lint(): void; 53 54 protected updateFileStats(faultId: number, line: number): void { 55 this.fileStats.nodeCounters[faultId]++; 56 this.fileStats.lineCounters[faultId].add(line); 57 } 58 59 protected static addLineColumnInfoInAutofix( 60 autofix: Autofix[], 61 startPos: ts.LineAndCharacter, 62 endPos: ts.LineAndCharacter 63 ): Autofix[] { 64 return autofix?.map((autofixElem) => { 65 autofixElem.line = startPos.line + 1; 66 autofixElem.column = startPos.character + 1; 67 autofixElem.endLine = endPos.line + 1; 68 autofixElem.endColumn = endPos.character + 1; 69 return autofixElem; 70 }); 71 } 72 73 protected incrementCounters( 74 node: ts.Node | ts.CommentRange, 75 faultId: number, 76 autofix?: Autofix[], 77 errorMsg?: string 78 ): void { 79 const badNodeInfo = this.getbadNodeInfo(node, faultId, autofix, errorMsg); 80 81 if (this.shouldSkipRule(badNodeInfo)) { 82 return; 83 } 84 85 this.problemsInfos.push(badNodeInfo); 86 this.updateFileStats(faultId, badNodeInfo.line); 87 // problems with autofixes might be collected separately 88 if (this.options.reportAutofixCb && badNodeInfo.autofix) { 89 this.options.reportAutofixCb(badNodeInfo); 90 } 91 } 92 93 private getbadNodeInfo( 94 node: ts.Node | ts.CommentRange, 95 faultId: number, 96 autofix?: Autofix[], 97 errorMsg?: string 98 ): ProblemInfo { 99 const [startOffset, endOffset] = TsUtils.getHighlightRange(node, faultId); 100 const startPos = this.sourceFile.getLineAndCharacterOfPosition(startOffset); 101 const endPos = this.sourceFile.getLineAndCharacterOfPosition(endOffset); 102 const faultDescr = faultDesc[faultId]; 103 const faultType = TypeScriptLinterConfig.tsSyntaxKindNames[node.kind]; 104 const cookBookMsgNum = faultsAttrs[faultId] ? faultsAttrs[faultId].cookBookRef : 0; 105 const cookBookTg = errorMsg ? errorMsg : cookBookTag[cookBookMsgNum]; 106 const severity = faultsAttrs[faultId]?.severity ?? ProblemSeverity.ERROR; 107 const isMsgNumValid = cookBookMsgNum > 0; 108 autofix = BaseTypeScriptLinter.processAutofix(autofix, startPos, endPos); 109 const badNodeInfo: ProblemInfo = { 110 line: startPos.line + 1, 111 column: startPos.character + 1, 112 endLine: endPos.line + 1, 113 endColumn: endPos.character + 1, 114 start: startOffset, 115 end: endOffset, 116 type: faultType, 117 severity: severity, 118 faultId: faultId, 119 problem: FaultID[faultId], 120 suggest: '', 121 // eslint-disable-next-line no-nested-ternary 122 rule: isMsgNumValid && cookBookTg !== '' ? cookBookTg : faultDescr ? faultDescr : faultType, 123 ruleTag: cookBookMsgNum, 124 autofixable: !!autofix, 125 autofix: autofix, 126 autofixTitle: isMsgNumValid && autofix !== undefined ? cookBookRefToFixTitle.get(cookBookMsgNum) : undefined 127 }; 128 return badNodeInfo; 129 } 130 131 private static processAutofix( 132 autofix: Autofix[] | undefined, 133 startPos: ts.LineAndCharacter, 134 endPos: ts.LineAndCharacter 135 ): Autofix[] | undefined { 136 return autofix ? BaseTypeScriptLinter.addLineColumnInfoInAutofix(autofix, startPos, endPos) : autofix; 137 } 138 139 private shouldSkipRule(badNodeInfo: ProblemInfo): boolean { 140 const ruleConfigTags = this.options.ruleConfigTags; 141 if (ruleConfigTags && !ruleConfigTags.has(badNodeInfo.ruleTag)) { 142 return true; 143 } 144 if (this.options?.ideInteractive) { 145 if (this.options.onlySyntax) { 146 if (onlyArkts2SyntaxRules.has(badNodeInfo.ruleTag)) { 147 return false; 148 } 149 } else if (this.options.arkts2 && arkts2Rules.includes(badNodeInfo.ruleTag)) { 150 return false; 151 } 152 return true; 153 } 154 return false; 155 } 156} 157