UNPKG

auto-cr-cmd

Version:

Fast automated code review CLI powered by SWC-based static analysis

559 lines (558 loc) 28.7 kB
#!/usr/bin/env node "use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var _a; Object.defineProperty(exports, "__esModule", { value: true }); var consola_1 = require("consola"); var fs_1 = __importDefault(require("fs")); var path_1 = __importDefault(require("path")); var commander_1 = require("commander"); var wasm_1 = require("@swc/wasm"); var config_1 = require("./config"); var report_1 = require("./report"); var i18n_1 = require("./i18n"); var file_1 = require("./utils/file"); var stdin_1 = require("./utils/stdin"); var auto_cr_rules_1 = require("auto-cr-rules"); var loader_1 = require("./rules/loader"); var autocrrc_1 = require("./config/autocrrc"); var ignore_1 = require("./config/ignore"); consola_1.consola.options.formatOptions = __assign(__assign({}, consola_1.consola.options.formatOptions), { date: false }); var consolaLoggers = { info: consola_1.consola.info.bind(consola_1.consola), warn: consola_1.consola.warn.bind(consola_1.consola), error: consola_1.consola.error.bind(consola_1.consola), }; function run() { return __awaiter(this, arguments, void 0, function (filePaths, ruleDir, format, configPath, ignorePath) { var t, notifications, log, validPaths, ignoreConfig, isIgnored_1, allFiles, _i, validPaths_1, targetPath, stat, directoryFiles, scannableFiles, customRules, rcConfig, rules, filesWithErrors, filesWithWarnings, filesWithOptimizing, totalViolations, totalErrorViolations, totalWarningViolations, totalOptimizingViolations, fileSummaries, _a, scannableFiles_1, file, summary, error_1; if (filePaths === void 0) { filePaths = []; } return __generator(this, function (_b) { switch (_b.label) { case 0: t = (0, i18n_1.getTranslator)(); notifications = []; log = function (level, message, detail) { var detailText; if (detail !== undefined) { if (detail instanceof Error) { detailText = detail.message; } else if (typeof detail === 'string') { detailText = detail; } else { try { detailText = JSON.stringify(detail); } catch (_a) { detailText = String(detail); } } } notifications.push({ level: level, message: message, detail: detailText }); if (format === 'text') { var logger = consolaLoggers[level]; if (detail === undefined) { logger(message); } else { logger(message, detail); } } }; _b.label = 1; case 1: _b.trys.push([1, 6, , 7]); if (filePaths.length === 0) { log('info', t.noPathsProvided()); return [2 /*return*/, { scannedFiles: 0, filesWithErrors: 0, filesWithWarnings: 0, filesWithOptimizing: 0, violationTotals: { total: 0, error: 0, warning: 0, optimizing: 0 }, files: [], notifications: notifications, }]; } validPaths = filePaths.filter(function (candidate) { return (0, file_1.checkPathExists)(candidate); }); if (validPaths.length === 0) { log('error', t.allPathsMissing()); return [2 /*return*/, { scannedFiles: 0, filesWithErrors: 0, filesWithWarnings: 0, filesWithOptimizing: 0, violationTotals: { total: 0, error: 0, warning: 0, optimizing: 0 }, files: [], notifications: notifications, }]; } ignoreConfig = (0, ignore_1.loadIgnoreConfig)(ignorePath); ignoreConfig.warnings.forEach(function (warning) { return log('warn', warning); }); isIgnored_1 = (0, ignore_1.createIgnoreMatcher)(ignoreConfig.patterns, ignoreConfig.baseDir); allFiles = []; for (_i = 0, validPaths_1 = validPaths; _i < validPaths_1.length; _i++) { targetPath = validPaths_1[_i]; if (isIgnored_1(targetPath)) { continue; } stat = fs_1.default.statSync(targetPath); if (stat.isFile()) { if (!isIgnored_1(targetPath)) { allFiles.push(targetPath); } } else if (stat.isDirectory()) { directoryFiles = (0, file_1.getAllFiles)(targetPath, [], ['.ts', '.tsx', '.js', '.jsx'], { shouldIgnore: function (fullPath) { return isIgnored_1(fullPath); }, }); allFiles = __spreadArray(__spreadArray([], allFiles, true), directoryFiles, true); } } if (allFiles.length === 0) { log('info', t.noFilesFound()); return [2 /*return*/, { scannedFiles: 0, filesWithErrors: 0, filesWithWarnings: 0, filesWithOptimizing: 0, violationTotals: { total: 0, error: 0, warning: 0, optimizing: 0 }, files: [], notifications: notifications, }]; } scannableFiles = allFiles.filter(function (candidate) { return !candidate.endsWith('.d.ts') && !isIgnored_1(candidate); }); customRules = (0, loader_1.loadCustomRules)(ruleDir); rcConfig = (0, autocrrc_1.loadAutoCrRc)(configPath); rcConfig.warnings.forEach(function (warning) { return log('warn', warning); }); rules = (0, autocrrc_1.applyRuleConfig)(__spreadArray(__spreadArray([], auto_cr_rules_1.builtinRules, true), customRules, true), rcConfig.rules, function (warning) { return log('warn', warning); }); if (rules.length === 0) { log('warn', rcConfig.rules ? t.autocrrcAllRulesDisabled() : t.noRulesLoaded()); return [2 /*return*/, { scannedFiles: 0, filesWithErrors: 0, filesWithWarnings: 0, filesWithOptimizing: 0, violationTotals: { total: 0, error: 0, warning: 0, optimizing: 0 }, files: [], notifications: notifications, }]; } filesWithErrors = 0; filesWithWarnings = 0; filesWithOptimizing = 0; totalViolations = 0; totalErrorViolations = 0; totalWarningViolations = 0; totalOptimizingViolations = 0; fileSummaries = []; _a = 0, scannableFiles_1 = scannableFiles; _b.label = 2; case 2: if (!(_a < scannableFiles_1.length)) return [3 /*break*/, 5]; file = scannableFiles_1[_a]; return [4 /*yield*/, analyzeFile(file, rules, format, log)]; case 3: summary = _b.sent(); if (summary.severityCounts.error > 0) { filesWithErrors += 1; } if (summary.severityCounts.warning > 0) { filesWithWarnings += 1; } if (summary.severityCounts.optimizing > 0) { filesWithOptimizing += 1; } totalViolations += summary.totalViolations; totalErrorViolations += summary.errorViolations; totalWarningViolations += summary.severityCounts.warning; totalOptimizingViolations += summary.severityCounts.optimizing; fileSummaries.push({ filePath: file, severityCounts: summary.severityCounts, totalViolations: summary.totalViolations, errorViolations: summary.errorViolations, violations: summary.violations, }); _b.label = 4; case 4: _a++; return [3 /*break*/, 2]; case 5: return [2 /*return*/, { scannedFiles: scannableFiles.length, filesWithErrors: filesWithErrors, filesWithWarnings: filesWithWarnings, filesWithOptimizing: filesWithOptimizing, violationTotals: { total: totalViolations, error: totalErrorViolations, warning: totalWarningViolations, optimizing: totalOptimizingViolations, }, files: fileSummaries, notifications: notifications, }]; case 6: error_1 = _b.sent(); throw error_1 instanceof Error ? error_1 : new Error(String(error_1)); case 7: return [2 /*return*/]; } }); }); } function analyzeFile(file, rules, format, log) { return __awaiter(this, void 0, void 0, function () { var source, reporter, t, ast, parseOptions, language, baseContext, sharedHelpers, _loop_1, _i, rules_1, rule, summary; return __generator(this, function (_a) { switch (_a.label) { case 0: source = (0, file_1.readFile)(file); reporter = (0, report_1.createReporter)(file, source, { format: format }); t = (0, i18n_1.getTranslator)(); try { parseOptions = (0, config_1.loadParseOptions)(file); ast = (0, wasm_1.parseSync)(source, parseOptions); } catch (error) { log('error', t.parseFileFailed({ file: file }), error); return [2 /*return*/, { severityCounts: { error: 1, warning: 0, optimizing: 0, }, totalViolations: 1, errorViolations: 1, violations: [], }]; } language = (0, i18n_1.getLanguage)(); baseContext = (0, auto_cr_rules_1.createRuleContext)({ ast: ast, filePath: file, source: source, reporter: reporter, language: language, }); sharedHelpers = baseContext.helpers; _loop_1 = function (rule) { var scopedReporter_1, reporterWithRecord_1, helpers, context, error_2; return __generator(this, function (_b) { switch (_b.label) { case 0: _b.trys.push([0, 2, , 3]); scopedReporter_1 = reporter.forRule(rule); reporterWithRecord_1 = scopedReporter_1; helpers = __assign(__assign({}, sharedHelpers), { reportViolation: (function (input, span) { var normalized = normalizeViolationInput(input, span); if (typeof reporterWithRecord_1.record === 'function') { reporterWithRecord_1.record({ description: normalized.message, code: normalized.code, suggestions: normalized.suggestions, span: normalized.span, line: normalized.line, }); return; } if (normalized.span) { scopedReporter_1.errorAtSpan(normalized.span, normalized.message); return; } if (typeof normalized.line === 'number') { scopedReporter_1.errorAtLine(normalized.line, normalized.message); return; } scopedReporter_1.error(normalized.message); }) }); context = __assign(__assign({}, baseContext), { reporter: scopedReporter_1, helpers: helpers }); return [4 /*yield*/, rule.run(context)]; case 1: _b.sent(); return [3 /*break*/, 3]; case 2: error_2 = _b.sent(); log('error', t.ruleExecutionFailed({ ruleName: rule.name, file: file }), error_2); return [3 /*break*/, 3]; case 3: return [2 /*return*/]; } }); }; _i = 0, rules_1 = rules; _a.label = 1; case 1: if (!(_i < rules_1.length)) return [3 /*break*/, 4]; rule = rules_1[_i]; return [5 /*yield**/, _loop_1(rule)]; case 2: _a.sent(); _a.label = 3; case 3: _i++; return [3 /*break*/, 1]; case 4: summary = reporter.flush(); return [2 /*return*/, { severityCounts: summary.severityCounts, totalViolations: summary.totalViolations, errorViolations: summary.errorViolations, violations: summary.violations, }]; } }); }); } function normalizeViolationInput(input, spanArg) { var _a; if (typeof input === 'string') { return { message: input, span: spanArg, }; } if (input && typeof input === 'object') { var candidate = input; var description = typeof candidate.description === 'string' ? candidate.description : typeof candidate.message === 'string' ? candidate.message : undefined; var code = typeof candidate.code === 'string' ? candidate.code : undefined; var suggestions = void 0; if (Array.isArray(candidate.suggestions)) { var normalizedSuggestions = []; for (var _i = 0, _b = candidate.suggestions; _i < _b.length; _i++) { var entry = _b[_i]; if (typeof entry === 'string') { normalizedSuggestions.push({ text: entry }); continue; } if (entry && typeof entry === 'object') { var suggestion = entry; if (typeof suggestion.text === 'string') { normalizedSuggestions.push({ text: suggestion.text, link: typeof suggestion.link === 'string' ? suggestion.link : undefined, }); } } } if (normalizedSuggestions.length > 0) { suggestions = normalizedSuggestions; } } return { message: description !== null && description !== void 0 ? description : 'Rule violation detected.', span: (_a = candidate.span) !== null && _a !== void 0 ? _a : spanArg, line: typeof candidate.line === 'number' ? candidate.line : undefined, code: code, suggestions: suggestions, }; } return { message: 'Rule violation detected.', span: spanArg, }; } function parseOutputFormat(value) { if (!value) { return 'text'; } var normalized = value.toLowerCase(); if (normalized === 'json' || normalized === 'text') { return normalized; } throw new Error("Unsupported output format: ".concat(value, ". Use \"text\" or \"json\".")); } function severityToLabel(severity) { switch (severity) { case auto_cr_rules_1.RuleSeverity.Warning: return 'warning'; case auto_cr_rules_1.RuleSeverity.Optimizing: return 'optimizing'; case auto_cr_rules_1.RuleSeverity.Error: default: return 'error'; } } function formatViolationForJson(violation) { var suggestions = violation.suggestions ? violation.suggestions.map(function (suggestion) { return (__assign({}, suggestion)); }) : []; var payload = { tag: violation.tag, ruleName: violation.ruleName, severity: severityToLabel(violation.severity), message: violation.message, suggestions: suggestions, }; if (typeof violation.line === 'number') { payload.line = violation.line; } if (violation.code) { payload.code = violation.code; } return payload; } function formatJsonOutput(result) { return { summary: { scannedFiles: result.scannedFiles, filesWithErrors: result.filesWithErrors, filesWithWarnings: result.filesWithWarnings, filesWithOptimizing: result.filesWithOptimizing, violationTotals: result.violationTotals, }, files: result.files.map(function (file) { return ({ filePath: file.filePath, severityCounts: file.severityCounts, totalViolations: file.totalViolations, errorViolations: file.errorViolations, violations: file.violations.map(formatViolationForJson), }); }), notifications: result.notifications, }; } commander_1.program .argument('[paths...]', '需要扫描的文件或目录路径列表 / Paths to scan') .option('-r, --rule-dir <directory>', '自定义规则目录路径 / Custom rule directory') .option('-l, --language <language>', '设置 CLI 语言 (zh/en) / Set CLI language (zh/en)') .option('-o, --output <format>', '设置输出格式 (text/json) / Output format (text/json)', 'text') .option('-c, --config <path>', '配置文件路径 (.autocrrc.json|.autocrrc.js) / Config file path (.autocrrc.json|.autocrrc.js)') .option('--ignore-path <path>', '忽略文件列表路径 (.autocrignore.json|.autocrignore.js) / Ignore file path (.autocrignore.json|.autocrignore.js)') .option('--tsconfig <path>', '自定义 tsconfig 路径 / Custom tsconfig path') .option('--stdin', '从标准输入读取扫描路径 / Read file paths from STDIN') .parse(process.argv); var options = commander_1.program.opts(); var cliArguments = commander_1.program.args; (0, i18n_1.setLanguage)((_a = options.language) !== null && _a !== void 0 ? _a : process.env.LANG); (0, config_1.setTsConfigPath)(options.tsconfig ? path_1.default.resolve(process.cwd(), options.tsconfig) : undefined); var outputFormat; try { outputFormat = parseOutputFormat(options.output); } catch (error) { var message = error instanceof Error ? error.message : String(error); consola_1.consola.error(message); process.exit(1); } ; (function () { return __awaiter(void 0, void 0, void 0, function () { var stdinTargets, combinedTargets, filePaths, result, t, payload, exitCode, language, resultMessage, exitCode, error_3, t, detail, payload; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 3, , 4]); return [4 /*yield*/, (0, stdin_1.readPathsFromStdin)(Boolean(options.stdin))]; case 1: stdinTargets = _a.sent(); combinedTargets = __spreadArray(__spreadArray([], cliArguments, true), stdinTargets, true); filePaths = combinedTargets.map(function (target) { return path_1.default.resolve(process.cwd(), target); }); return [4 /*yield*/, run(filePaths, options.ruleDir, outputFormat, options.config, options.ignorePath)]; case 2: result = _a.sent(); t = (0, i18n_1.getTranslator)(); if (outputFormat === 'json') { payload = formatJsonOutput(result); exitCode = result.filesWithErrors > 0 ? 1 : 0; process.stdout.write("".concat(JSON.stringify(payload, null, 2), "\n")); process.exit(exitCode); } if (result.scannedFiles > 0) { consola_1.consola.log(' '); language = (0, i18n_1.getLanguage)(); resultMessage = language.startsWith('zh') ? " ".concat(t.scanComplete(), "\uFF0C\u672C\u6B21\u5171\u626B\u63CF").concat(result.scannedFiles, "\u4E2A\u6587\u4EF6\uFF0C\u5176\u4E2D").concat(result.filesWithErrors, "\u4E2A\u6587\u4EF6\u5B58\u5728\u9519\u8BEF\uFF0C").concat(result.filesWithWarnings, "\u4E2A\u6587\u4EF6\u5B58\u5728\u8B66\u544A\uFF0C").concat(result.filesWithOptimizing, "\u4E2A\u6587\u4EF6\u5B58\u5728\u4F18\u5316\u5EFA\u8BAE\uFF01") : " ".concat(t.scanComplete(), ", scanned ").concat(result.scannedFiles, " files: ").concat(result.filesWithErrors, " with errors, ").concat(result.filesWithWarnings, " with warnings, ").concat(result.filesWithOptimizing, " with optimizing hints!"); consola_1.consola.success(resultMessage); exitCode = result.filesWithErrors > 0 ? 1 : 0; process.exit(exitCode); } else { process.exit(0); } return [3 /*break*/, 4]; case 3: error_3 = _a.sent(); t = (0, i18n_1.getTranslator)(); detail = error_3 instanceof Error ? error_3.message : String(error_3); if (outputFormat === 'json') { payload = { error: { message: t.scanError(), detail: detail, }, }; process.stdout.write("".concat(JSON.stringify(payload, null, 2), "\n")); } else { consola_1.consola.error(t.scanError(), detail); } process.exit(1); return [3 /*break*/, 4]; case 4: return [2 /*return*/]; } }); }); })();