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