• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2023-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 { OptionValues } from 'commander';
17import { Command, Option } from 'commander';
18import * as fs from 'node:fs';
19import * as path from 'node:path';
20import * as ts from 'typescript';
21import type { CommandLineOptions } from '../lib/CommandLineOptions';
22import { cookBookTag } from '../lib/CookBookMsg';
23import { Logger } from '../lib/Logger';
24import { ARKTS_IGNORE_DIRS_OH_MODULES } from '../lib/utils/consts/ArktsIgnorePaths';
25import { getConfiguredRuleTags, getRulesFromConfig } from '../lib/utils/functions/ConfiguredRulesProcess';
26import { extractRuleTags } from '../lib/utils/functions/CookBookUtils';
27import { logTscDiagnostic } from '../lib/utils/functions/LogTscDiagnostic';
28
29const TS_EXT = '.ts';
30const TSX_EXT = '.tsx';
31const ETS_EXT = '.ets';
32const JS_EXT = '.js';
33
34interface CommanderParseOptions {
35  exitOnFail?: boolean;
36  disableErrorOutput?: boolean;
37}
38
39interface ProcessedArguments {
40  inputFiles: string[];
41  responseFiles: string[];
42}
43
44interface ParsedCommand {
45  opts: OptionValues;
46  args: ProcessedArguments;
47}
48
49const getFiles = (dir: string): string[] => {
50  const resultFiles: string[] = [];
51  if (dir.includes(ARKTS_IGNORE_DIRS_OH_MODULES)) {
52    return [];
53  }
54
55  const files = fs.readdirSync(dir);
56  for (let i = 0; i < files.length; ++i) {
57    const name = path.join(dir, files[i]);
58    if (fs.statSync(name).isDirectory()) {
59      resultFiles.push(...getFiles(name));
60    } else {
61      const extension = path.extname(name);
62      if (extension === TS_EXT || extension === TSX_EXT || extension === ETS_EXT || extension === JS_EXT) {
63        resultFiles.push(name);
64      }
65    }
66  }
67
68  return resultFiles;
69};
70
71function addProjectFolder(projectFolder: string, previous: string[]): string[] {
72  return previous.concat([projectFolder]);
73}
74
75function processProgramArguments(args: string[]): ProcessedArguments {
76  const processed: ProcessedArguments = {
77    inputFiles: [],
78    responseFiles: []
79  };
80  for (const arg of args) {
81    if (arg.startsWith('@')) {
82      processed.responseFiles.push(arg.slice(1));
83    } else {
84      processed.inputFiles.push(arg);
85    }
86  }
87  return processed;
88}
89
90function parseCommand(program: Command, cmdArgs: string[]): ParsedCommand {
91  program.parse(cmdArgs);
92  return {
93    opts: program.opts<OptionValues>(),
94    args: processProgramArguments(program.args)
95  };
96}
97
98function formSdkOptions(cmdOptions: CommandLineOptions, commanderOpts: OptionValues): void {
99  if (commanderOpts.sdkExternalApiPath) {
100    cmdOptions.sdkExternalApiPath = commanderOpts.sdkExternalApiPath;
101  }
102  if (commanderOpts.sdkDefaultApiPath) {
103    cmdOptions.sdkDefaultApiPath = commanderOpts.sdkDefaultApiPath;
104  }
105  if (commanderOpts.arktsWholeProjectPath) {
106    cmdOptions.arktsWholeProjectPath = commanderOpts.arktsWholeProjectPath;
107  }
108}
109
110function formMigrateOptions(cmdOptions: CommandLineOptions, commanderOpts: OptionValues): void {
111  if (commanderOpts.migrate) {
112    cmdOptions.linterOptions.migratorMode = true;
113    cmdOptions.linterOptions.enableAutofix = true;
114  }
115  if (commanderOpts.migrationBackupFile === false) {
116    cmdOptions.linterOptions.noMigrationBackupFile = true;
117  }
118  if (commanderOpts.migrationMaxPass) {
119    const num = Number(commanderOpts.migrationMaxPass);
120    cmdOptions.linterOptions.migrationMaxPass = isNaN(num) ? 0 : num;
121  }
122  if (commanderOpts.migrationReport) {
123    cmdOptions.linterOptions.migrationReport = true;
124  }
125  if (commanderOpts.arktsWholeProjectPath) {
126    cmdOptions.linterOptions.wholeProjectPath = commanderOpts.arktsWholeProjectPath;
127  }
128}
129
130function formIdeInteractive(cmdOptions: CommandLineOptions, commanderOpts: OptionValues): void {
131  if (commanderOpts.ideInteractive) {
132    cmdOptions.linterOptions.ideInteractive = true;
133  }
134  if (commanderOpts.checkTsAndJs) {
135    cmdOptions.linterOptions.checkTsAndJs = true;
136  }
137  if (commanderOpts.onlyArkts2SyntaxRules) {
138    cmdOptions.linterOptions.onlySyntax = true;
139  }
140  if (commanderOpts.autofixCheck) {
141    cmdOptions.linterOptions.autofixCheck = true;
142  }
143}
144
145function formArkts2Options(cmdOptions: CommandLineOptions, commanderOpts: OptionValues): void {
146  if (commanderOpts.arkts2) {
147    cmdOptions.linterOptions.arkts2 = true;
148  }
149  if (commanderOpts.skipLinter) {
150    cmdOptions.skipLinter = true;
151  }
152  if (commanderOpts.homecheck) {
153    cmdOptions.homecheck = true;
154  }
155  if (commanderOpts.outputFilePath) {
156    cmdOptions.outputFilePath = path.normalize(commanderOpts.outputFilePath);
157  }
158  if (commanderOpts.verbose) {
159    cmdOptions.verbose = true;
160  }
161  if (commanderOpts.enableInterop) {
162    cmdOptions.scanWholeProjectInHomecheck = true;
163  }
164}
165
166function formCommandLineOptions(parsedCmd: ParsedCommand): CommandLineOptions {
167  const opts: CommandLineOptions = {
168    inputFiles: parsedCmd.args.inputFiles.map((file) => {
169      return path.normalize(file);
170    }),
171    linterOptions: {
172      useRtLogic: true,
173      interopCheckMode: false
174    }
175  };
176  const options = parsedCmd.opts;
177  if (options.TSC_Errors) {
178    opts.logTscErrors = true;
179  }
180  if (options.devecoPluginMode) {
181    opts.devecoPluginModeDeprecated = true;
182  }
183  if (options.checkTsAsSource !== undefined) {
184    opts.linterOptions.checkTsAsSource = options.checkTsAsSource;
185  }
186  if (options.projectFolder) {
187    doProjectFolderArg(options.projectFolder, opts);
188    opts.linterOptions.projectFolderList = options.projectFolder;
189  }
190  if (options.project) {
191    doProjectArg(options.project, opts);
192  }
193  if (options.autofix) {
194    opts.linterOptions.enableAutofix = true;
195  }
196  if (options.warningsAsErrors) {
197    opts.linterOptions.warningsAsErrors = true;
198  }
199  if (options.useRtLogic !== undefined) {
200    opts.linterOptions.useRtLogic = options.useRtLogic;
201  }
202  processRuleConfig(opts, options);
203  formIdeInteractive(opts, options);
204  formSdkOptions(opts, options);
205  formMigrateOptions(opts, options);
206  formArkts2Options(opts, options);
207  return opts;
208}
209
210function processRuleConfig(commandLineOptions: CommandLineOptions, options: OptionValues): void {
211  if (options.ruleConfig !== undefined) {
212    const stats = fs.statSync(path.normalize(options.ruleConfig));
213    if (!stats.isFile()) {
214      console.error(`The file at ${options.ruleConfigPath} path does not exist!`);
215    } else {
216      const configuredRulesMap = getRulesFromConfig(options.ruleConfig);
217      const arkTSRulesMap = extractRuleTags(cookBookTag);
218      commandLineOptions.linterOptions.ruleConfigTags = getConfiguredRuleTags(arkTSRulesMap, configuredRulesMap);
219    }
220  }
221}
222
223function createCommand(): Command {
224  const program = new Command();
225  program.
226    name('tslinter').
227    description('Linter for TypeScript sources').
228    version('0.0.2').
229    configureHelp({ helpWidth: 100 }).
230    exitOverride();
231  program.
232    option('-E, --TSC_Errors', 'show error messages from Tsc').
233    option('--check-ts-as-source', 'check TS files as source files').
234    option('-p, --project <project_file>', 'path to TS project config file').
235    option(
236      '-f, --project-folder <project_folder>',
237      'path to folder containing TS files to verify',
238      addProjectFolder,
239      []
240    ).
241    option('--autofix', 'automatically fix problems found by linter').
242    option('--arkts-2', 'enable ArkTS 2.0 mode').
243    option('--use-rt-logic', 'run linter with RT logic').
244    option('-e, --sdk-external-api-path <paths...>', 'paths to external API files').
245    option('-d, --sdk-default-api-path <path>', 'paths to default API files').
246    option('--ide-interactive', 'Migration Helper IDE interactive mode').
247    option('-w, --arkts-whole-project-path <path>', 'path to whole project').
248    option('--migrate', 'run as ArkTS migrator').
249    option('--skip-linter', 'skip linter rule validation and autofix').
250    option('--homecheck', 'added homecheck rule validation').
251    option('--no-migration-backup-file', 'Disable the backup files in migration mode').
252    option('--migration-max-pass <num>', 'Maximum number of migration passes').
253    option('--migration-report', 'Generate migration report').
254    option('--check-ts-and-js', 'check ts and js files').
255    option('--only-arkts2-syntax-rules', 'only syntax rules').
256    option('-o, --output-file-path <path>', 'path to store all log and result files').
257    option('--verbose', 'set log level to see debug messages').
258    option('--enable-interop', 'scan whole project to report 1.1 import 1.2').
259    option('--rule-config <path>', 'Path to the rule configuration file').
260    option('--autofix-check', 'confirm whether the user needs automatic repair').
261    addOption(new Option('--warnings-as-errors', 'treat warnings as errors').hideHelp(true)).
262    addOption(new Option('--no-check-ts-as-source', 'check TS files as third-party libary').hideHelp(true)).
263    addOption(new Option('--no-use-rt-logic', 'run linter with SDK logic').hideHelp(true)).
264    addOption(new Option('--deveco-plugin-mode', 'run as IDE plugin (obsolete)').hideHelp(true));
265  program.argument('[srcFile...]', 'files to be verified');
266  return program;
267}
268
269export function parseCommandLine(
270  commandLineArgs: string[],
271  commanderParseOpts: CommanderParseOptions = {}
272): CommandLineOptions {
273  const { exitOnFail = true, disableErrorOutput = false } = commanderParseOpts;
274  const program = createCommand();
275  if (disableErrorOutput) {
276    program.configureOutput({
277      writeErr: () => {},
278      writeOut: () => {}
279    });
280  }
281
282  // method parse() eats two first args, so make them dummy
283  const cmdArgs: string[] = ['dummy', 'dummy'];
284  cmdArgs.push(...commandLineArgs);
285  let parsedCmd: ParsedCommand;
286  try {
287    parsedCmd = parseCommand(program, cmdArgs);
288  } catch (error) {
289    if (exitOnFail) {
290      process.exit(-1);
291    }
292    throw error;
293  }
294  processResponseFiles(parsedCmd);
295  return formCommandLineOptions(parsedCmd);
296}
297
298function processResponseFiles(parsedCmd: ParsedCommand): void {
299  if (!parsedCmd.args.responseFiles.length) {
300    return;
301  }
302  const rspFiles = parsedCmd.args.responseFiles;
303  for (const rspFile of rspFiles) {
304    try {
305      const rspArgs = fs.
306        readFileSync(rspFile).
307        toString().
308        split('\n').
309        filter((e) => {
310          return e.trimEnd();
311        });
312      const cmdArgs = ['dummy', 'dummy'];
313      cmdArgs.push(...rspArgs);
314      const parsedRsp = parseCommand(createCommand(), cmdArgs);
315      Object.assign(parsedCmd.opts, parsedRsp.opts);
316      parsedCmd.args.inputFiles.push(...parsedRsp.args.inputFiles);
317    } catch (error) {
318      Logger.error('Failed to read response file: ' + error);
319      process.exit(-1);
320    }
321  }
322}
323
324function doProjectFolderArg(prjFolders: string[], opts: CommandLineOptions): void {
325  for (let i = 0; i < prjFolders.length; i++) {
326    const prjFolderPath = prjFolders[i];
327    try {
328      opts.inputFiles.push(...getFiles(prjFolderPath));
329    } catch (error) {
330      Logger.error('Failed to read folder: ' + error);
331      process.exit(-1);
332    }
333  }
334}
335
336function doProjectArg(cfgPath: string, opts: CommandLineOptions): void {
337  // Process project file (tsconfig.json) and retrieve config arguments.
338  const configFile = cfgPath;
339
340  const host: ts.ParseConfigFileHost = ts.sys as ts.System & ts.ParseConfigFileHost;
341
342  const diagnostics: ts.Diagnostic[] = [];
343
344  try {
345    const oldUnrecoverableDiagnostic = host.onUnRecoverableConfigFileDiagnostic;
346    host.onUnRecoverableConfigFileDiagnostic = (diagnostic: ts.Diagnostic): void => {
347      diagnostics.push(diagnostic);
348    };
349    opts.parsedConfigFile = ts.getParsedCommandLineOfConfigFile(configFile, {}, host);
350    host.onUnRecoverableConfigFileDiagnostic = oldUnrecoverableDiagnostic;
351
352    if (opts.parsedConfigFile) {
353      diagnostics.push(...ts.getConfigFileParsingDiagnostics(opts.parsedConfigFile));
354    }
355
356    if (diagnostics.length > 0) {
357      // Log all diagnostic messages and exit program.
358      Logger.error('Failed to read config file.');
359      logTscDiagnostic(diagnostics, Logger.info);
360      process.exit(-1);
361    }
362  } catch (error) {
363    Logger.error('Failed to read config file: ' + error);
364    process.exit(-1);
365  }
366}
367