* Copyright (c) 2022-2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as ts from 'typescript';
import * as qEd from './autofixes/QuasiEditor';
import type { BaseTypeScriptLinter } from './BaseTypeScriptLinter';
import type { CommandLineOptions } from './CommandLineOptions';
import { InteropTypescriptLinter } from './InteropTypescriptLinter';
import type { LinterConfig } from './LinterConfig';
import type { LinterOptions } from './LinterOptions';
import type { LintRunResult } from './LintRunResult';
import { Logger } from './Logger';
import type { ProblemInfo } from './ProblemInfo';
import type { CmdProgressInfo } from './progress/CmdProgressInfo';
import {
FixedLineProgressBar,
postProcessCmdProgressBar,
preProcessCmdProgressBar,
processCmdProgressBar
} from './progress/FixedLineProgressBar';
import type { MigrationInfo } from './progress/MigrationInfo';
import type { ProgressBarInfo } from './progress/ProgressBarInfo';
import { ProjectStatistics } from './statistics/ProjectStatistics';
import { generateMigrationStatisicsReport } from './statistics/scan/ProblemStatisticsCommonFunction';
import type { TimeRecorder } from './statistics/scan/TimeRecorder';
import type { createProgramCallback } from './ts-compiler/Compiler';
import { compileLintOptions } from './ts-compiler/Compiler';
import { getTscDiagnostics } from './ts-diagnostics/GetTscDiagnostics';
import { transformTscDiagnostics } from './ts-diagnostics/TransformTscDiagnostics';
import { TypeScriptLinter } from './TypeScriptLinter';
import {
ARKTS_IGNORE_DIRS_NO_OH_MODULES,
ARKTS_IGNORE_DIRS_OH_MODULES,
ARKTS_IGNORE_FILES
} from './utils/consts/ArktsIgnorePaths';
import { EXTNAME_JS, EXTNAME_TS } from './utils/consts/ExtensionName';
import { USE_STATIC } from './utils/consts/InteropAPI';
import { LibraryTypeCallDiagnosticChecker } from './utils/functions/LibraryTypeCallDiagnosticChecker';
import { mergeArrayMaps } from './utils/functions/MergeArrayMaps';
import { clearPathHelperCache, pathContainsDirectory } from './utils/functions/PathHelper';
import { processSyncErr } from './utils/functions/ProcessWrite';
import type { LinterInputInfo } from './LinterInputInfo';
import { collectCommonApiInfo, clearCommonApiInfoCache } from './utils/functions/CommonApiInfo';
import { tryCallGC, tryForceCallGC } from './utils/functions/GarbageCollectorUtils';
function prepareInputFilesList(cmdOptions: CommandLineOptions): string[] {
let inputFiles = cmdOptions.inputFiles.map((x) => {
return path.normalize(x);
});
if (!cmdOptions.isCommandConfig || !cmdOptions.parsedConfigFile) {
return inputFiles;
}
inputFiles = cmdOptions.parsedConfigFile.fileNames;
if (cmdOptions.inputFiles.length <= 0) {
return inputFiles;
}
* Apply linter only to the project source files that are specified
* as a command-line arguments. Other source files will be discarded.
*/
const cmdInputsResolvedPaths = cmdOptions.inputFiles.map((x) => {
return path.resolve(x);
});
const configInputsResolvedPaths = inputFiles.map((x) => {
return path.resolve(x);
});
inputFiles = configInputsResolvedPaths.filter((x) => {
return cmdInputsResolvedPaths.some((y) => {
return x === y;
});
});
return inputFiles;
}
export function lint(
config: LinterConfig,
timeRecorder: TimeRecorder,
hcResults?: Map<string, ProblemInfo[]>
): LintRunResult {
const lintResult = lintImpl(config);
timeRecorder.endScan();
return config.cmdOptions.linterOptions.migratorMode ?
migrate(config, lintResult, timeRecorder, hcResults) :
lintResult;
}
function lintImpl(config: LinterConfig, migrationInfo?: MigrationInfo): LintRunResult {
const { cmdOptions, tscCompiledProgram } = config;
const tsProgram = tscCompiledProgram.getProgram();
const options = cmdOptions.linterOptions;
let inputFiles = prepareInputFilesList(cmdOptions);
inputFiles = inputFiles.filter((input) => {
return shouldProcessFile(options, input);
});
options.inputFiles = inputFiles;
const srcFiles: ts.SourceFile[] = [];
for (const inputFile of inputFiles) {
const srcFile = tsProgram.getSourceFile(inputFile);
if (srcFile) {
srcFiles.push(srcFile);
}
collectCommonApiInfo(tsProgram);
}
const tscStrictDiagnostics = getTscDiagnostics(tscCompiledProgram, srcFiles);
LibraryTypeCallDiagnosticChecker.instance.rebuildTscDiagnostics(tscStrictDiagnostics);
const lintResult = lintFiles(tsProgram, srcFiles, options, tscStrictDiagnostics, migrationInfo);
LibraryTypeCallDiagnosticChecker.instance.clear();
if (!options.ideInteractive) {
lintResult.problemsInfos = mergeArrayMaps(lintResult.problemsInfos, transformTscDiagnostics(tscStrictDiagnostics));
}
freeMemory();
return lintResult;
}
function lintFiles(
tsProgram: ts.Program,
srcFiles: ts.SourceFile[],
options: LinterOptions,
tscStrictDiagnostics: Map<string, ts.Diagnostic[]>,
migrationInfo?: MigrationInfo
): LintRunResult {
TypeScriptLinter.initGlobals();
InteropTypescriptLinter.initGlobals();
const cmdProgressBar = new FixedLineProgressBar();
const cmdProgressInfo: CmdProgressInfo = {
cmdProgressBar: cmdProgressBar,
migrationInfo: migrationInfo,
srcFiles: srcFiles,
options: options
};
if (options.ideInteractive) {
process.stderr.write('\n');
preProcessCmdProgressBar(cmdProgressInfo);
}
const linterInputInfo: LinterInputInfo = {
tsProgram: tsProgram,
srcFiles: srcFiles,
options: options,
tscStrictDiagnostics: tscStrictDiagnostics,
migrationInfo: migrationInfo,
cmdProgressInfo: cmdProgressInfo
};
const lintResult = executeLinter(linterInputInfo);
if (options.ideInteractive) {
postProcessCmdProgressBar(cmdProgressInfo);
}
return lintResult;
}
function executeLinter(linterInputInfo: LinterInputInfo): LintRunResult {
const { tsProgram, srcFiles, options, tscStrictDiagnostics, migrationInfo, cmdProgressInfo } = linterInputInfo;
const projectStats: ProjectStatistics = new ProjectStatistics();
const problemsInfos: Map<string, ProblemInfo[]> = new Map();
let fileCount: number = 0;
for (const srcFile of srcFiles) {
const linter: BaseTypeScriptLinter = !options.interopCheckMode ?
new TypeScriptLinter(tsProgram.getTypeChecker(), options, srcFile, tscStrictDiagnostics) :
new InteropTypescriptLinter(tsProgram.getTypeChecker(), tsProgram.getCompilerOptions(), options, srcFile);
if (migrationInfo?.migrateforUI && linter instanceof TypeScriptLinter) {
linter.lintForUI();
} else {
linter.lint();
}
const problems = linter.problemsInfos;
problemsInfos.set(path.normalize(srcFile.fileName), [...problems]);
projectStats.fileStats.push(linter.fileStats);
fileCount += 1;
if (options.ideInteractive) {
processCmdProgressBar(cmdProgressInfo, fileCount);
processIdeProgressBar(
{ migrationInfo: migrationInfo, currentSrcFile: srcFile, srcFiles: srcFiles, options: options },
fileCount,
cmdProgressInfo.cmdProgressBar
);
}
}
return {
hasErrors: projectStats.hasError(),
problemsInfos: problemsInfos,
projectStats: projectStats
};
}
export function processIdeProgressBar(
progressBarInfo: ProgressBarInfo,
fileCount: number,
cmdProgressBar: FixedLineProgressBar
): void {
const { currentSrcFile, srcFiles, options } = progressBarInfo;
const isMigrationStep = options.migratorMode && progressBarInfo.migrationInfo;
const phasePrefix = isMigrationStep ? 'Migration Phase' : 'Scan Phase';
const migrationPhase = isMigrationStep ?
` ${(progressBarInfo.migrationInfo!.currentPass ?? 0) + 1} / ${progressBarInfo.migrationInfo!.maxPasses ?? 1}` :
'';
const progressRatio = srcFiles.length > 0 ? fileCount / srcFiles.length : 0;
const displayContent = `currentFile: ${currentSrcFile ? currentSrcFile.fileName : 'N/A'}, ${phasePrefix}${migrationPhase}`;
const displayInfo = cmdProgressBar.getDisplayInfo();
process.stderr.write('\x1B7');
const linesToMove = displayInfo.totalReserved - displayInfo.ideLine;
process.stderr.write(`\x1B[${linesToMove}A`);
process.stderr.write('\x1B[2K');
processSyncErr(
JSON.stringify({
content: displayContent,
messageType: 1,
indicator: progressRatio
}) + '\n'
);
process.stderr.write('\x1B8');
}
function migrate(
linterConfig: LinterConfig,
lintResult: LintRunResult,
timeRecorder: TimeRecorder,
hcResults?: Map<string, ProblemInfo[]>
): LintRunResult {
timeRecorder.startMigration();
const { cmdOptions } = linterConfig;
const updatedSourceTexts: Map<string, string> = new Map();
tryForceCallGC();
let mergedProblems: Map<string, ProblemInfo[]> = hcResults ?? new Map();
mergedProblems = mergeArrayMaps(
mergedProblems,
filterLinterProblemsWithAutofixConfig(linterConfig.cmdOptions, lintResult.problemsInfos)
);
const changedFiles = fix(linterConfig, updatedSourceTexts, mergedProblems);
if (changedFiles.length !== 0) {
cmdOptions.inputFiles = changedFiles;
updateLinterConfig(cmdOptions, linterConfig, updatedSourceTexts);
const lintUIResults = lintImpl(linterConfig, { migrateforUI: true });
fix(linterConfig, updatedSourceTexts, mergedProblems, lintUIResults.problemsInfos);
}
updateSourceFiles(updatedSourceTexts, cmdOptions);
timeRecorder.endMigration();
generateMigrationStatisicsReport(mergedProblems, timeRecorder, cmdOptions.outputFilePath);
if (!cmdOptions.linterOptions.ideInteractive) {
updateLinterConfig(cmdOptions, linterConfig, updatedSourceTexts);
return lintImpl(linterConfig);
}
lintResult.problemsInfos = mergedProblems;
return lintResult;
}
function updateLinterConfig(
cmdOptions: CommandLineOptions,
linterConfig: LinterConfig,
updatedSourceTexts: Map<string, string>
): void {
const newLinterConfig = compileLintOptions(cmdOptions, getMigrationCreateProgramCallback(updatedSourceTexts));
linterConfig.tscCompiledProgram = newLinterConfig.tscCompiledProgram;
tryCallGC();
}
function filterLinterProblemsWithAutofixConfig(
cmdOptions: CommandLineOptions,
problemsInfos: Map<string, ProblemInfo[]>
): Map<string, ProblemInfo[]> {
const autofixRuleConfigTags = cmdOptions.linterOptions.autofixRuleConfigTags;
if (!cmdOptions.linterOptions.ideInteractive || !autofixRuleConfigTags) {
return problemsInfos;
}
const needToBeFixedProblemsInfos = new Map<string, ProblemInfo[]>();
for (const [filePath, problems] of problemsInfos) {
const needToFix: ProblemInfo[] = problems.filter((problem) => {
return autofixRuleConfigTags.has(problem.ruleTag);
});
if (needToFix.length >= 0) {
needToBeFixedProblemsInfos.set(filePath, needToFix);
}
}
return needToBeFixedProblemsInfos;
}
function updateSourceFiles(updatedSourceTexts: Map<string, string>, cmdOptions: CommandLineOptions): void {
updatedSourceTexts.forEach((newText, fileName) => {
if (!cmdOptions.linterOptions.noMigrationBackupFile) {
qEd.QuasiEditor.backupSrcFile(fileName);
}
const filePathMap = cmdOptions.linterOptions.migrationFilePathMap;
const writeFileName = filePathMap?.get(fileName) ?? fileName;
fs.writeFileSync(writeFileName, newText);
});
}
function hasUseStaticDirective(srcFile: ts.SourceFile): boolean {
if (!srcFile?.statements.length) {
return false;
}
const statements = srcFile.statements;
return (
ts.isExpressionStatement(statements[0]) &&
ts.isStringLiteral(statements[0].expression) &&
statements[0].expression.getText() === USE_STATIC
);
}
function fix(
linterConfig: LinterConfig,
updatedSourceTexts: Map<string, string>,
mergedProblems: Map<string, ProblemInfo[]>,
lintUIProblems?: Map<string, ProblemInfo[]>
): string[] {
const program = linterConfig.tscCompiledProgram.getProgram();
const changedFiles: string[] = [];
if (lintUIProblems) {
mergedProblems = lintUIProblems;
}
mergedProblems.forEach((problemInfos, fileName) => {
const srcFile = program.getSourceFile(fileName);
if (!srcFile) {
if (!linterConfig.cmdOptions.homecheck) {
Logger.error(`Failed to retrieve source file: ${fileName}`);
}
return;
}
const hasAnyAutofixes = qEd.QuasiEditor.hasAnyAutofixes(problemInfos);
const needToAddUseStatic =
linterConfig.cmdOptions.linterOptions.arkts2 &&
linterConfig.cmdOptions.inputFiles.includes(fileName) &&
!hasUseStaticDirective(srcFile) &&
linterConfig.cmdOptions.linterOptions.ideInteractive &&
(!hasAnyAutofixes || !!lintUIProblems);
if (!hasAnyAutofixes && !needToAddUseStatic) {
return;
}
const qe: qEd.QuasiEditor = new qEd.QuasiEditor(
fileName,
srcFile.text,
linterConfig.cmdOptions.linterOptions,
linterConfig.cmdOptions.outputFilePath
);
updatedSourceTexts.set(fileName, qe.fix(problemInfos, needToAddUseStatic));
if (hasAnyAutofixes) {
changedFiles.push(fileName);
}
});
return changedFiles;
}
function getMigrationCreateProgramCallback(updatedSourceTexts: Map<string, string>): createProgramCallback {
return (createProgramOptions: ts.CreateProgramOptions): ts.Program => {
const compilerHost = createProgramOptions.host || ts.createCompilerHost(createProgramOptions.options, true);
const originalReadFile = compilerHost.readFile;
compilerHost.readFile = (fileName: string): string | undefined => {
const newText = updatedSourceTexts.get(path.normalize(fileName));
return newText || originalReadFile(fileName);
};
createProgramOptions.host = compilerHost;
return ts.createProgram(createProgramOptions);
};
}
export function shouldProcessFile(options: LinterOptions, fileFsPath: string): boolean {
if (!options.checkTsAndJs && (path.extname(fileFsPath) === EXTNAME_TS || path.extname(fileFsPath) === EXTNAME_JS)) {
return false;
}
if (
ARKTS_IGNORE_FILES.some((ignore) => {
return path.basename(fileFsPath) === ignore;
})
) {
return false;
}
if (
ARKTS_IGNORE_DIRS_NO_OH_MODULES.some((ignore) => {
return pathContainsDirectory(path.resolve(fileFsPath), ignore);
})
) {
return false;
}
return (
!pathContainsDirectory(path.resolve(fileFsPath), ARKTS_IGNORE_DIRS_OH_MODULES) ||
!!options.isFileFromModuleCb?.(fileFsPath)
);
}
function freeMemory(): void {
clearPathHelperCache();
clearCommonApiInfoCache();
}