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