1/* 2 * Copyright (c) 2022-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 * as fs from 'node:fs'; 17import * as path from 'node:path'; 18import * as ts from 'typescript'; 19import { processSyncErr } from '../lib/utils/functions/ProcessWrite'; 20import * as qEd from './autofixes/QuasiEditor'; 21import type { BaseTypeScriptLinter } from './BaseTypeScriptLinter'; 22import type { CommandLineOptions } from './CommandLineOptions'; 23import { InteropTypescriptLinter } from './InteropTypescriptLinter'; 24import type { LinterConfig } from './LinterConfig'; 25import type { LinterOptions } from './LinterOptions'; 26import type { LintRunResult } from './LintRunResult'; 27import { Logger } from './Logger'; 28import type { ProblemInfo } from './ProblemInfo'; 29import { ProjectStatistics } from './statistics/ProjectStatistics'; 30import { generateMigrationStatisicsReport } from './statistics/scan/ProblemStatisticsCommonFunction'; 31import type { TimeRecorder } from './statistics/scan/TimeRecorder'; 32import type { createProgramCallback } from './ts-compiler/Compiler'; 33import { compileLintOptions } from './ts-compiler/Compiler'; 34import { getTscDiagnostics } from './ts-diagnostics/GetTscDiagnostics'; 35import { transformTscDiagnostics } from './ts-diagnostics/TransformTscDiagnostics'; 36import { TypeScriptLinter } from './TypeScriptLinter'; 37import { 38 ARKTS_IGNORE_DIRS_NO_OH_MODULES, 39 ARKTS_IGNORE_DIRS_OH_MODULES, 40 ARKTS_IGNORE_FILES 41} from './utils/consts/ArktsIgnorePaths'; 42import { EXTNAME_JS, EXTNAME_TS } from './utils/consts/ExtensionName'; 43import { USE_STATIC } from './utils/consts/InteropAPI'; 44import { LibraryTypeCallDiagnosticChecker } from './utils/functions/LibraryTypeCallDiagnosticChecker'; 45import { mergeArrayMaps } from './utils/functions/MergeArrayMaps'; 46import { clearPathHelperCache, pathContainsDirectory } from './utils/functions/PathHelper'; 47 48function prepareInputFilesList(cmdOptions: CommandLineOptions): string[] { 49 let inputFiles = cmdOptions.inputFiles.map((x) => { 50 return path.normalize(x); 51 }); 52 if (!cmdOptions.parsedConfigFile) { 53 return inputFiles; 54 } 55 56 inputFiles = cmdOptions.parsedConfigFile.fileNames; 57 if (cmdOptions.inputFiles.length <= 0) { 58 return inputFiles; 59 } 60 61 /* 62 * Apply linter only to the project source files that are specified 63 * as a command-line arguments. Other source files will be discarded. 64 */ 65 const cmdInputsResolvedPaths = cmdOptions.inputFiles.map((x) => { 66 return path.resolve(x); 67 }); 68 const configInputsResolvedPaths = inputFiles.map((x) => { 69 return path.resolve(x); 70 }); 71 inputFiles = configInputsResolvedPaths.filter((x) => { 72 return cmdInputsResolvedPaths.some((y) => { 73 return x === y; 74 }); 75 }); 76 77 return inputFiles; 78} 79 80export function lint( 81 config: LinterConfig, 82 timeRecorder: TimeRecorder, 83 etsLoaderPath?: string, 84 hcResults?: Map<string, ProblemInfo[]> 85): LintRunResult { 86 if (etsLoaderPath) { 87 config.cmdOptions.linterOptions.etsLoaderPath = etsLoaderPath; 88 } 89 const lintResult = lintImpl(config); 90 timeRecorder.endScan(); 91 return config.cmdOptions.linterOptions.migratorMode ? 92 migrate(config, lintResult, timeRecorder, hcResults) : 93 lintResult; 94} 95 96function lintImpl(config: LinterConfig): LintRunResult { 97 const { cmdOptions, tscCompiledProgram } = config; 98 const tsProgram = tscCompiledProgram.getProgram(); 99 const options = cmdOptions.linterOptions; 100 101 // Prepare list of input files for linter and retrieve AST for those files. 102 let inputFiles = prepareInputFilesList(cmdOptions); 103 inputFiles = inputFiles.filter((input) => { 104 return shouldProcessFile(options, input); 105 }); 106 options.inputFiles = inputFiles; 107 const srcFiles: ts.SourceFile[] = []; 108 for (const inputFile of inputFiles) { 109 const srcFile = tsProgram.getSourceFile(inputFile); 110 if (srcFile) { 111 srcFiles.push(srcFile); 112 } 113 } 114 115 const tscStrictDiagnostics = getTscDiagnostics(tscCompiledProgram, srcFiles); 116 LibraryTypeCallDiagnosticChecker.instance.rebuildTscDiagnostics(tscStrictDiagnostics); 117 const lintResult = lintFiles(tsProgram, srcFiles, options, tscStrictDiagnostics); 118 LibraryTypeCallDiagnosticChecker.instance.clear(); 119 120 if (!options.ideInteractive) { 121 lintResult.problemsInfos = mergeArrayMaps(lintResult.problemsInfos, transformTscDiagnostics(tscStrictDiagnostics)); 122 } 123 124 freeMemory(); 125 return lintResult; 126} 127 128function lintFiles( 129 tsProgram: ts.Program, 130 srcFiles: ts.SourceFile[], 131 options: LinterOptions, 132 tscStrictDiagnostics: Map<string, ts.Diagnostic[]> 133): LintRunResult { 134 const projectStats: ProjectStatistics = new ProjectStatistics(); 135 const problemsInfos: Map<string, ProblemInfo[]> = new Map(); 136 137 TypeScriptLinter.initGlobals(); 138 InteropTypescriptLinter.initGlobals(); 139 let fileCount: number = 0; 140 141 for (const srcFile of srcFiles) { 142 const linter: BaseTypeScriptLinter = !options.interopCheckMode ? 143 new TypeScriptLinter(tsProgram.getTypeChecker(), options, srcFile, tscStrictDiagnostics) : 144 new InteropTypescriptLinter(tsProgram.getTypeChecker(), tsProgram.getCompilerOptions(), options, srcFile); 145 linter.lint(); 146 const problems = linter.problemsInfos; 147 problemsInfos.set(path.normalize(srcFile.fileName), [...problems]); 148 projectStats.fileStats.push(linter.fileStats); 149 fileCount = fileCount + 1; 150 if (options.ideInteractive) { 151 processSyncErr(`{"content":"${srcFile.fileName}","messageType":1,"indicator":${fileCount / srcFiles.length}}\n`); 152 } 153 } 154 155 return { 156 hasErrors: projectStats.hasError(), 157 problemsInfos, 158 projectStats 159 }; 160} 161 162function migrate( 163 initialConfig: LinterConfig, 164 initialLintResult: LintRunResult, 165 timeRecorder: TimeRecorder, 166 hcResults?: Map<string, ProblemInfo[]> 167): LintRunResult { 168 timeRecorder.startMigration(); 169 let linterConfig = initialConfig; 170 const { cmdOptions } = initialConfig; 171 const updatedSourceTexts: Map<string, string> = new Map(); 172 let lintResult: LintRunResult = initialLintResult; 173 const problemsInfosBeforeMigrate = lintResult.problemsInfos; 174 175 for (let pass = 0; pass < (cmdOptions.linterOptions.migrationMaxPass ?? qEd.DEFAULT_MAX_AUTOFIX_PASSES); pass++) { 176 const appliedFix = fix(linterConfig, lintResult, updatedSourceTexts, hcResults); 177 hcResults = undefined; 178 179 if (!appliedFix) { 180 // No fixes were applied, migration is finished. 181 break; 182 } 183 184 // Re-compile and re-lint project after applying the fixes. 185 linterConfig = compileLintOptions(cmdOptions, getMigrationCreateProgramCallback(updatedSourceTexts)); 186 lintResult = lintImpl(linterConfig); 187 } 188 189 // Write new text for updated source files. 190 updatedSourceTexts.forEach((newText, fileName) => { 191 if (!cmdOptions.linterOptions.noMigrationBackupFile) { 192 qEd.QuasiEditor.backupSrcFile(fileName); 193 } 194 const filePathMap = cmdOptions.linterOptions.migrationFilePathMap; 195 const writeFileName = filePathMap?.get(fileName) ?? fileName; 196 fs.writeFileSync(writeFileName, newText); 197 }); 198 199 timeRecorder.endMigration(); 200 generateMigrationStatisicsReport(lintResult, timeRecorder, cmdOptions.outputFilePath); 201 202 if (cmdOptions.linterOptions.ideInteractive) { 203 lintResult.problemsInfos = problemsInfosBeforeMigrate; 204 } 205 206 return lintResult; 207} 208 209function hasUseStaticDirective(srcFile: ts.SourceFile): boolean { 210 if (!srcFile?.statements.length) { 211 return false; 212 } 213 const statements = srcFile.statements; 214 return ( 215 ts.isExpressionStatement(statements[0]) && 216 ts.isStringLiteral(statements[0].expression) && 217 statements[0].expression.getText() === USE_STATIC 218 ); 219} 220 221function fix( 222 linterConfig: LinterConfig, 223 lintResult: LintRunResult, 224 updatedSourceTexts: Map<string, string>, 225 hcResults?: Map<string, ProblemInfo[]> 226): boolean { 227 const program = linterConfig.tscCompiledProgram.getProgram(); 228 let appliedFix = false; 229 // Apply homecheck fixes first to avoid them being skipped due to conflict with linter autofixes 230 let mergedProblems: Map<string, ProblemInfo[]> = hcResults ?? new Map(); 231 mergedProblems = mergeArrayMaps(mergedProblems, lintResult.problemsInfos); 232 mergedProblems.forEach((problemInfos, fileName) => { 233 const srcFile = program.getSourceFile(fileName); 234 if (!srcFile) { 235 if (!linterConfig.cmdOptions.homecheck) { 236 Logger.error(`Failed to retrieve source file: ${fileName}`); 237 } 238 return; 239 } 240 const needToAddUseStatic = 241 linterConfig.cmdOptions.linterOptions.arkts2 && 242 linterConfig.cmdOptions.inputFiles.includes(fileName) && 243 !hasUseStaticDirective(srcFile) && 244 linterConfig.cmdOptions.linterOptions.ideInteractive && 245 !qEd.QuasiEditor.hasAnyAutofixes(problemInfos); 246 // If nothing to fix or don't need to add 'use static', then skip file 247 if (!qEd.QuasiEditor.hasAnyAutofixes(problemInfos) && !needToAddUseStatic) { 248 return; 249 } 250 const qe: qEd.QuasiEditor = new qEd.QuasiEditor( 251 fileName, 252 srcFile.text, 253 linterConfig.cmdOptions.linterOptions, 254 undefined, 255 linterConfig.cmdOptions.outputFilePath 256 ); 257 updatedSourceTexts.set(fileName, qe.fix(problemInfos, needToAddUseStatic)); 258 if (!needToAddUseStatic) { 259 appliedFix = true; 260 } 261 }); 262 263 return appliedFix; 264} 265 266function getMigrationCreateProgramCallback(updatedSourceTexts: Map<string, string>): createProgramCallback { 267 return (createProgramOptions: ts.CreateProgramOptions): ts.Program => { 268 const compilerHost = createProgramOptions.host || ts.createCompilerHost(createProgramOptions.options, true); 269 const originalReadFile = compilerHost.readFile; 270 compilerHost.readFile = (fileName: string): string | undefined => { 271 const newText = updatedSourceTexts.get(path.normalize(fileName)); 272 return newText || originalReadFile(fileName); 273 }; 274 createProgramOptions.host = compilerHost; 275 return ts.createProgram(createProgramOptions); 276 }; 277} 278 279export function shouldProcessFile(options: LinterOptions, fileFsPath: string): boolean { 280 if (!options.checkTsAndJs && (path.extname(fileFsPath) === EXTNAME_TS || path.extname(fileFsPath) === EXTNAME_JS)) { 281 return false; 282 } 283 284 if ( 285 ARKTS_IGNORE_FILES.some((ignore) => { 286 return path.basename(fileFsPath) === ignore; 287 }) 288 ) { 289 return false; 290 } 291 292 if ( 293 ARKTS_IGNORE_DIRS_NO_OH_MODULES.some((ignore) => { 294 return pathContainsDirectory(path.resolve(fileFsPath), ignore); 295 }) 296 ) { 297 return false; 298 } 299 300 return ( 301 !pathContainsDirectory(path.resolve(fileFsPath), ARKTS_IGNORE_DIRS_OH_MODULES) || 302 !!options.isFileFromModuleCb?.(fileFsPath) 303 ); 304} 305 306function freeMemory(): void { 307 clearPathHelperCache(); 308} 309