• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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