• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2023-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 { Logger } from '../lib/Logger';
17import { logTscDiagnostic } from '../lib/utils/functions/LogTscDiagnostic';
18import type { CommandLineOptions } from '../lib/CommandLineOptions';
19import type { OptionValues } from 'commander';
20import { Command, Option } from 'commander';
21import * as ts from 'typescript';
22import * as fs from 'node:fs';
23import * as path from 'node:path';
24
25const TS_EXT = '.ts';
26const TSX_EXT = '.tsx';
27const ETS_EXT = '.ets';
28
29interface CommanderParseOptions {
30  exitOnFail?: boolean;
31  disableErrorOutput?: boolean;
32}
33
34interface ProcessedArguments {
35  inputFiles: string[];
36  responseFiles: string[];
37}
38
39interface ParsedCommand {
40  opts: OptionValues;
41  args: ProcessedArguments;
42}
43
44const getFiles = (dir: string): string[] => {
45  const resultFiles: string[] = [];
46
47  const files = fs.readdirSync(dir);
48  for (let i = 0; i < files.length; ++i) {
49    const name = path.join(dir, files[i]);
50    if (fs.statSync(name).isDirectory()) {
51      resultFiles.push(...getFiles(name));
52    } else {
53      const extension = path.extname(name);
54      if (extension === TS_EXT || extension === TSX_EXT || extension === ETS_EXT) {
55        resultFiles.push(name);
56      }
57    }
58  }
59
60  return resultFiles;
61};
62
63function addProjectFolder(projectFolder: string, previous: string[]): string[] {
64  return previous.concat([projectFolder]);
65}
66
67function processProgramArguments(args: string[]): ProcessedArguments {
68  const processed: ProcessedArguments = {
69    inputFiles: [],
70    responseFiles: []
71  };
72  for (const arg of args) {
73    if (arg.startsWith('@')) {
74      processed.responseFiles.push(arg.slice(1));
75    } else {
76      processed.inputFiles.push(arg);
77    }
78  }
79  return processed;
80}
81
82function parseCommand(program: Command, cmdArgs: string[]): ParsedCommand {
83  program.parse(cmdArgs);
84  return {
85    opts: program.opts<OptionValues>(),
86    args: processProgramArguments(program.args)
87  };
88}
89
90function formCommandLineOptions(parsedCmd: ParsedCommand): CommandLineOptions {
91  const opts: CommandLineOptions = {
92    inputFiles: parsedCmd.args.inputFiles,
93    linterOptions: {
94      useRtLogic: true,
95      interopCheckMode: false
96    }
97  };
98  const options = parsedCmd.opts;
99  if (options.TSC_Errors) {
100    opts.logTscErrors = true;
101  }
102  if (options.devecoPluginMode) {
103    opts.linterOptions.ideMode = true;
104  }
105  if (options.checkTsAsSource !== undefined) {
106    opts.linterOptions.checkTsAsSource = options.checkTsAsSource;
107  }
108  if (options.projectFolder) {
109    doProjectFolderArg(options.projectFolder, opts);
110  }
111  if (options.project) {
112    doProjectArg(options.project, opts);
113  }
114  if (options.autofix) {
115    opts.linterOptions.enableAutofix = true;
116  }
117  if (options.arkts2) {
118    opts.linterOptions.arkts2 = true;
119  }
120  if (options.warningsAsErrors) {
121    opts.linterOptions.warningsAsErrors = true;
122  }
123  if (options.useRtLogic !== undefined) {
124    opts.linterOptions.useRtLogic = options.useRtLogic;
125  }
126  return opts;
127}
128
129function createCommand(): Command {
130  const program = new Command();
131  program.
132    name('tslinter').
133    description('Linter for TypeScript sources').
134    version('0.0.1').
135    configureHelp({ helpWidth: 100 }).
136    exitOverride();
137  program.
138    option('-E, --TSC_Errors', 'show error messages from Tsc').
139    option('--check-ts-as-source', 'check TS files as source files').
140    option('--deveco-plugin-mode', 'run as IDE plugin').
141    option('-p, --project <project_file>', 'path to TS project config file').
142    option(
143      '-f, --project-folder <project_folder>',
144      'path to folder containing TS files to verify',
145      addProjectFolder,
146      []
147    ).
148    option('--autofix', 'automatically fix problems found by linter').
149    option('--arkts-2', 'enable ArkTS 2.0 mode').
150    option('--use-rt-logic', 'run linter with RT logic').
151    addOption(new Option('--warnings-as-errors', 'treat warnings as errors').hideHelp(true)).
152    addOption(new Option('--no-check-ts-as-source', 'check TS files as third-party libary').hideHelp(true)).
153    addOption(new Option('--no-use-rt-logic', 'run linter with SDK logic').hideHelp(true)).
154    addOption(new Option('--deveco-plugin-mode', 'run as IDE plugin (obsolete)').hideHelp(true));
155  program.argument('[srcFile...]', 'files to be verified');
156  return program;
157}
158
159export function parseCommandLine(
160  commandLineArgs: string[],
161  commanderParseOpts: CommanderParseOptions = {}
162): CommandLineOptions {
163  const { exitOnFail = true, disableErrorOutput = false } = commanderParseOpts;
164  const program = createCommand();
165  if (disableErrorOutput) {
166    program.configureOutput({
167      writeErr: () => {},
168      writeOut: () => {}
169    });
170  }
171
172  // method parse() eats two first args, so make them dummy
173  const cmdArgs: string[] = ['dummy', 'dummy'];
174  cmdArgs.push(...commandLineArgs);
175  let parsedCmd: ParsedCommand;
176  try {
177    parsedCmd = parseCommand(program, cmdArgs);
178  } catch (error) {
179    if (exitOnFail) {
180      process.exit(-1);
181    }
182    throw error;
183  }
184  processResponseFiles(parsedCmd);
185  return formCommandLineOptions(parsedCmd);
186}
187
188function processResponseFiles(parsedCmd: ParsedCommand): void {
189  if (!parsedCmd.args.responseFiles.length) {
190    return;
191  }
192  const rspFiles = parsedCmd.args.responseFiles;
193  for (const rspFile of rspFiles) {
194    try {
195      const rspArgs = fs.
196        readFileSync(rspFile).
197        toString().
198        split('\n').
199        filter((e) => {
200          return e.trimEnd();
201        });
202      const cmdArgs = ['dummy', 'dummy'];
203      cmdArgs.push(...rspArgs);
204      const parsedRsp = parseCommand(createCommand(), cmdArgs);
205      Object.assign(parsedCmd.opts, parsedRsp.opts);
206      parsedCmd.args.inputFiles.push(...parsedRsp.args.inputFiles);
207    } catch (error) {
208      Logger.error('Failed to read response file: ' + error);
209      process.exit(-1);
210    }
211  }
212}
213
214function doProjectFolderArg(prjFolders: string[], opts: CommandLineOptions): void {
215  for (let i = 0; i < prjFolders.length; i++) {
216    const prjFolderPath = prjFolders[i];
217    try {
218      opts.inputFiles.push(...getFiles(prjFolderPath));
219    } catch (error) {
220      Logger.error('Failed to read folder: ' + error);
221      process.exit(-1);
222    }
223  }
224}
225
226function doProjectArg(cfgPath: string, opts: CommandLineOptions): void {
227  // Process project file (tsconfig.json) and retrieve config arguments.
228  const configFile = cfgPath;
229
230  const host: ts.ParseConfigFileHost = ts.sys as ts.System & ts.ParseConfigFileHost;
231
232  const diagnostics: ts.Diagnostic[] = [];
233
234  try {
235    const oldUnrecoverableDiagnostic = host.onUnRecoverableConfigFileDiagnostic;
236    host.onUnRecoverableConfigFileDiagnostic = (diagnostic: ts.Diagnostic): void => {
237      diagnostics.push(diagnostic);
238    };
239    opts.parsedConfigFile = ts.getParsedCommandLineOfConfigFile(configFile, {}, host);
240    host.onUnRecoverableConfigFileDiagnostic = oldUnrecoverableDiagnostic;
241
242    if (opts.parsedConfigFile) {
243      diagnostics.push(...ts.getConfigFileParsingDiagnostics(opts.parsedConfigFile));
244    }
245
246    if (diagnostics.length > 0) {
247      // Log all diagnostic messages and exit program.
248      Logger.error('Failed to read config file.');
249      logTscDiagnostic(diagnostics, Logger.info);
250      process.exit(-1);
251    }
252  } catch (error) {
253    Logger.error('Failed to read config file: ' + error);
254    process.exit(-1);
255  }
256}
257