1/*
2 * Copyright (c) 2022-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
16import * as path from 'node:path';
17import type * as ts from 'typescript';
18import type { CommandLineOptions } from './CommandLineOptions';
19import { faultsAttrs } from './FaultAttrs';
20import { faultDesc } from './FaultDesc';
21import { InteropTypescriptLinter } from './InteropTypescriptLinter';
22import type { LintOptions } from './LintOptions';
23import type { LintRunResult } from './LintRunResult';
24import type { ProblemInfo } from './ProblemInfo';
25import { ProblemSeverity } from './ProblemSeverity';
26import { FaultID } from './Problems';
27import { TypeScriptLinter, consoleLog } from './TypeScriptLinter';
28import { getTscDiagnostics } from './ts-diagnostics/GetTscDiagnostics';
29import { transformTscDiagnostics } from './ts-diagnostics/TransformTscDiagnostics';
30import {
31  ARKTS_IGNORE_DIRS_NO_OH_MODULES,
32  ARKTS_IGNORE_DIRS_OH_MODULES,
33  ARKTS_IGNORE_FILES
34} from './utils/consts/ArktsIgnorePaths';
35import { mergeArrayMaps } from './utils/functions/MergeArrayMaps';
36import { clearPathHelperCache, pathContainsDirectory } from './utils/functions/PathHelper';
37import { LibraryTypeCallDiagnosticChecker } from './utils/functions/LibraryTypeCallDiagnosticChecker';
38
39function prepareInputFilesList(cmdOptions: CommandLineOptions): string[] {
40  let inputFiles = cmdOptions.inputFiles;
41  if (!cmdOptions.parsedConfigFile) {
42    return inputFiles;
43  }
44
45  inputFiles = cmdOptions.parsedConfigFile.fileNames;
46  if (cmdOptions.inputFiles.length <= 0) {
47    return inputFiles;
48  }
49
50  /*
51   * Apply linter only to the project source files that are specified
52   * as a command-line arguments. Other source files will be discarded.
53   */
54  const cmdInputsResolvedPaths = cmdOptions.inputFiles.map((x) => {
55    return path.resolve(x);
56  });
57  const configInputsResolvedPaths = inputFiles.map((x) => {
58    return path.resolve(x);
59  });
60  inputFiles = configInputsResolvedPaths.filter((x) => {
61    return cmdInputsResolvedPaths.some((y) => {
62      return x === y;
63    });
64  });
65
66  return inputFiles;
67}
68
69function countProblems(linter: TypeScriptLinter | InteropTypescriptLinter): [number, number] {
70  let errorNodesTotal = 0;
71  let warningNodes = 0;
72  for (let i = 0; i < FaultID.LAST_ID; i++) {
73    switch (faultsAttrs[i].severity) {
74      case ProblemSeverity.ERROR:
75        errorNodesTotal += linter.nodeCounters[i];
76        break;
77      case ProblemSeverity.WARNING:
78        warningNodes += linter.nodeCounters[i];
79        break;
80      default:
81    }
82  }
83
84  return [errorNodesTotal, warningNodes];
85}
86
87// eslint-disable-next-line max-lines-per-function
88export function lint(options: LintOptions, etsLoaderPath: string | undefined): LintRunResult {
89  const cmdOptions = options.cmdOptions;
90  const tscCompiledProgram = options.tscCompiledProgram;
91  const tsProgram = tscCompiledProgram.getProgram();
92
93  // Prepare list of input files for linter and retrieve AST for those files.
94  let inputFiles = prepareInputFilesList(cmdOptions);
95  inputFiles = inputFiles.filter((input) => {
96    return shouldProcessFile(options, input);
97  });
98  const srcFiles: ts.SourceFile[] = [];
99  for (const inputFile of inputFiles) {
100    const srcFile = tsProgram.getSourceFile(inputFile);
101    if (srcFile) {
102      srcFiles.push(srcFile);
103    }
104  }
105
106  const tscStrictDiagnostics = getTscDiagnostics(tscCompiledProgram, srcFiles);
107  LibraryTypeCallDiagnosticChecker.rebuildTscDiagnostics(tscStrictDiagnostics);
108  const linter = options.isEtsFile ?
109    new TypeScriptLinter(
110      tsProgram.getTypeChecker(),
111      cmdOptions.enableAutofix,
112      cmdOptions.arkts2,
113      options.cancellationToken,
114      options.incrementalLintInfo,
115      tscStrictDiagnostics,
116      options.reportAutofixCb,
117      options.isEtsFileCb,
118      options.compatibleSdkVersion,
119      options.compatibleSdkVersionStage
120    ) :
121    new InteropTypescriptLinter(
122      tsProgram.getTypeChecker(),
123      tsProgram.getCompilerOptions(),
124      cmdOptions.arkts2,
125      etsLoaderPath
126    );
127  const { errorNodes, problemsInfos } = lintFiles(srcFiles, linter);
128  consoleLog('\n\n\nFiles scanned: ', srcFiles.length);
129  consoleLog('\nFiles with problems: ', errorNodes);
130
131  const [errorNodesTotal, warningNodes] = countProblems(linter);
132  logTotalProblemsInfo(errorNodesTotal, warningNodes, linter);
133  logProblemsPercentageByFeatures(linter);
134
135  freeMemory();
136
137  return {
138    errorNodes: errorNodesTotal,
139    problemsInfos: mergeArrayMaps(problemsInfos, transformTscDiagnostics(tscStrictDiagnostics))
140  };
141}
142
143function lintFiles(srcFiles: ts.SourceFile[], linter: TypeScriptLinter | InteropTypescriptLinter): LintRunResult {
144  let problemFiles = 0;
145  const problemsInfos: Map<string, ProblemInfo[]> = new Map();
146
147  for (const srcFile of srcFiles) {
148    const prevVisitedNodes = linter.totalVisitedNodes;
149    const prevErrorLines = linter.totalErrorLines;
150    const prevWarningLines = linter.totalWarningLines;
151    linter.errorLineNumbersString = '';
152    linter.warningLineNumbersString = '';
153    const nodeCounters: number[] = [];
154
155    for (let i = 0; i < FaultID.LAST_ID; i++) {
156      nodeCounters[i] = linter.nodeCounters[i];
157    }
158
159    linter.lint(srcFile);
160    // save results and clear problems array
161    problemsInfos.set(path.normalize(srcFile.fileName), [...linter.problemsInfos]);
162    linter.problemsInfos.length = 0;
163
164    // print results for current file
165    const fileVisitedNodes = linter.totalVisitedNodes - prevVisitedNodes;
166    const fileErrorLines = linter.totalErrorLines - prevErrorLines;
167    const fileWarningLines = linter.totalWarningLines - prevWarningLines;
168
169    problemFiles = countProblemFiles(
170      nodeCounters,
171      problemFiles,
172      srcFile,
173      fileVisitedNodes,
174      fileErrorLines,
175      fileWarningLines,
176      linter
177    );
178  }
179
180  return {
181    errorNodes: problemFiles,
182    problemsInfos: problemsInfos
183  };
184}
185
186// eslint-disable-next-line max-lines-per-function, max-params
187function countProblemFiles(
188  nodeCounters: number[],
189  filesNumber: number,
190  tsSrcFile: ts.SourceFile,
191  fileNodes: number,
192  fileErrorLines: number,
193  fileWarningLines: number,
194  linter: TypeScriptLinter | InteropTypescriptLinter
195): number {
196  let errorNodes = 0;
197  let warningNodes = 0;
198  for (let i = 0; i < FaultID.LAST_ID; i++) {
199    const nodeCounterDiff = linter.nodeCounters[i] - nodeCounters[i];
200    switch (faultsAttrs[i].severity) {
201      case ProblemSeverity.ERROR:
202        errorNodes += nodeCounterDiff;
203        break;
204      case ProblemSeverity.WARNING:
205        warningNodes += nodeCounterDiff;
206        break;
207      default:
208    }
209  }
210  if (errorNodes > 0) {
211    // eslint-disable-next-line no-param-reassign
212    filesNumber++;
213    const errorRate = (errorNodes / fileNodes * 100).toFixed(2);
214    const warningRate = (warningNodes / fileNodes * 100).toFixed(2);
215    consoleLog(tsSrcFile.fileName, ': ', '\n\tError lines: ', linter.errorLineNumbersString);
216    consoleLog(tsSrcFile.fileName, ': ', '\n\tWarning lines: ', linter.warningLineNumbersString);
217    consoleLog(
218      '\n\tError constructs (%): ',
219      errorRate,
220      '\t[ of ',
221      fileNodes,
222      ' constructs ], \t',
223      fileErrorLines,
224      ' lines'
225    );
226    consoleLog(
227      '\n\tWarning constructs (%): ',
228      warningRate,
229      '\t[ of ',
230      fileNodes,
231      ' constructs ], \t',
232      fileWarningLines,
233      ' lines'
234    );
235  }
236
237  return filesNumber;
238}
239
240function logTotalProblemsInfo(
241  errorNodes: number,
242  warningNodes: number,
243  linter: TypeScriptLinter | InteropTypescriptLinter
244): void {
245  const errorRate = (errorNodes / linter.totalVisitedNodes * 100).toFixed(2);
246  const warningRate = (warningNodes / linter.totalVisitedNodes * 100).toFixed(2);
247  consoleLog('\nTotal error constructs (%): ', errorRate);
248  consoleLog('\nTotal warning constructs (%): ', warningRate);
249  consoleLog('\nTotal error lines:', linter.totalErrorLines, ' lines\n');
250  consoleLog('\nTotal warning lines:', linter.totalWarningLines, ' lines\n');
251}
252
253function logProblemsPercentageByFeatures(linter: TypeScriptLinter | InteropTypescriptLinter): void {
254  consoleLog('\nPercent by features: ');
255  for (let i = 0; i < FaultID.LAST_ID; i++) {
256    const nodes = linter.nodeCounters[i];
257    const lines = linter.lineCounters[i];
258    const pecentage = (nodes / linter.totalVisitedNodes * 100).toFixed(2).padEnd(7, ' ');
259
260    consoleLog(faultDesc[i].padEnd(55, ' '), pecentage, '[', nodes, ' constructs / ', lines, ' lines]');
261  }
262}
263
264function shouldProcessFile(options: LintOptions, fileFsPath: string): boolean {
265  if (
266    ARKTS_IGNORE_FILES.some((ignore) => {
267      return path.basename(fileFsPath) === ignore;
268    })
269  ) {
270    return false;
271  }
272
273  if (
274    ARKTS_IGNORE_DIRS_NO_OH_MODULES.some((ignore) => {
275      return pathContainsDirectory(path.resolve(fileFsPath), ignore);
276    })
277  ) {
278    return false;
279  }
280
281  return (
282    !pathContainsDirectory(path.resolve(fileFsPath), ARKTS_IGNORE_DIRS_OH_MODULES) ||
283    !!options.isFileFromModuleCb?.(fileFsPath)
284  );
285}
286
287function freeMemory(): void {
288  clearPathHelperCache();
289}
290