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 { Command, Option } from 'commander';
20import * as ts from 'typescript';
21import * as fs from 'node:fs';
22import * as path from 'node:path';
23
24const TS_EXT = '.ts';
25const TSX_EXT = '.tsx';
26const ETS_EXT = '.ets';
27
28let inputFiles: string[];
29let responseFile = '';
30function addSrcFile(value: string): void {
31  if (value.startsWith('@')) {
32    responseFile = value;
33  } else {
34    inputFiles.push(value);
35  }
36}
37
38const getFiles = (dir: string): string[] => {
39  const resultFiles: string[] = [];
40
41  const files = fs.readdirSync(dir);
42  for (let i = 0; i < files.length; ++i) {
43    const name = path.join(dir, files[i]);
44    if (fs.statSync(name).isDirectory()) {
45      resultFiles.push(...getFiles(name));
46    } else {
47      const extension = path.extname(name);
48      if (extension === TS_EXT || extension === TSX_EXT || extension === ETS_EXT) {
49        resultFiles.push(name);
50      }
51    }
52  }
53
54  return resultFiles;
55};
56
57function addProjectFolder(projectFolder: string, previous: string[]): string[] {
58  return previous.concat([projectFolder]);
59}
60
61function formCommandLineOptions(program: Command): CommandLineOptions {
62  const opts: CommandLineOptions = {
63    inputFiles: inputFiles,
64    warningsAsErrors: false,
65    enableAutofix: false,
66    arkts2: false
67  };
68  const options = program.opts();
69  if (options.TSC_Errors) {
70    opts.logTscErrors = true;
71  }
72  if (options.devecoPluginMode) {
73    opts.ideMode = true;
74  }
75  if (options.testMode) {
76    opts.testMode = true;
77  }
78  if (options.projectFolder) {
79    doProjectFolderArg(options.projectFolder, opts);
80  }
81  if (options.project) {
82    doProjectArg(options.project, opts);
83  }
84  if (options.autofix) {
85    opts.enableAutofix = true;
86  }
87  if (options.arkts2) {
88    opts.arkts2 = true;
89  }
90  if (options.warningsAsErrors) {
91    opts.warningsAsErrors = true;
92  }
93  return opts;
94}
95
96export function parseCommandLine(commandLineArgs: string[]): CommandLineOptions {
97  const program = new Command();
98  program.name('tslinter').description('Linter for TypeScript sources').
99    version('0.0.1');
100  program.
101    option('-E, --TSC_Errors', 'show error messages from Tsc').
102    option('--test-mode', 'run linter as if running TS test files').
103    option('--deveco-plugin-mode', 'run as IDE plugin').
104    option('-p, --project <project_file>', 'path to TS project config file').
105    option('--project-folder <project_folder>', 'path to folder containig TS files to verify', addProjectFolder, []).
106    option('--autofix', 'automatically fix problems found by linter').
107    option('--arkts-2', 'enable ArkTS 2.0 mode').
108    addOption(new Option('--warnings-as-errors', 'treat warnings as errors').hideHelp(true));
109  program.argument('[srcFile...]', 'files to be verified', addSrcFile);
110
111  inputFiles = [];
112  // method parse() eats two first args, so make them dummy
113  let cmdArgs: string[] = ['dummy', 'dummy'];
114  cmdArgs.push(...commandLineArgs);
115  program.parse(cmdArgs);
116  if (responseFile !== '') {
117    try {
118      // eslint-disable-next-line no-param-reassign
119      commandLineArgs = fs.
120        readFileSync(responseFile.slice(1)).
121        toString().
122        split('\n').
123        filter((e) => {
124          return e.trimEnd();
125        });
126      cmdArgs = ['dummy', 'dummy'];
127      cmdArgs.push(...commandLineArgs);
128      program.parse(cmdArgs);
129    } catch (error) {
130      Logger.error('Failed to read response file: ' + error);
131      process.exit(-1);
132    }
133  }
134
135  return formCommandLineOptions(program);
136}
137
138function doProjectFolderArg(prjFolders: string[], opts: CommandLineOptions): void {
139  for (let i = 0; i < prjFolders.length; i++) {
140    const prjFolderPath = prjFolders[i];
141    try {
142      opts.inputFiles.push(...getFiles(prjFolderPath));
143    } catch (error) {
144      Logger.error('Failed to read folder: ' + error);
145      process.exit(-1);
146    }
147  }
148}
149
150function doProjectArg(cfgPath: string, opts: CommandLineOptions): void {
151  // Process project file (tsconfig.json) and retrieve config arguments.
152  const configFile = cfgPath;
153
154  const host: ts.ParseConfigFileHost = ts.sys as ts.System & ts.ParseConfigFileHost;
155
156  const diagnostics: ts.Diagnostic[] = [];
157
158  try {
159    const oldUnrecoverableDiagnostic = host.onUnRecoverableConfigFileDiagnostic;
160    host.onUnRecoverableConfigFileDiagnostic = (diagnostic: ts.Diagnostic): void => {
161      diagnostics.push(diagnostic);
162    };
163    opts.parsedConfigFile = ts.getParsedCommandLineOfConfigFile(configFile, {}, host);
164    host.onUnRecoverableConfigFileDiagnostic = oldUnrecoverableDiagnostic;
165
166    if (opts.parsedConfigFile) {
167      diagnostics.push(...ts.getConfigFileParsingDiagnostics(opts.parsedConfigFile));
168    }
169
170    if (diagnostics.length > 0) {
171      // Log all diagnostic messages and exit program.
172      Logger.error('Failed to read config file.');
173      logTscDiagnostic(diagnostics, Logger.info);
174      process.exit(-1);
175    }
176  } catch (error) {
177    Logger.error('Failed to read config file: ' + error);
178    process.exit(-1);
179  }
180}
181