frontend-guardian-core 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bin/fg-core.js +1238 -0
- package/bin/watch-mode.js +123 -0
- package/dist/engine/cache.d.ts +68 -0
- package/dist/engine/cache.d.ts.map +1 -0
- package/dist/engine/cache.js +164 -0
- package/dist/engine/cache.js.map +1 -0
- package/dist/engine/rule-engine.d.ts +135 -0
- package/dist/engine/rule-engine.d.ts.map +1 -0
- package/dist/engine/rule-engine.js +716 -0
- package/dist/engine/rule-engine.js.map +1 -0
- package/dist/formatters/github-annotation.d.ts +36 -0
- package/dist/formatters/github-annotation.d.ts.map +1 -0
- package/dist/formatters/github-annotation.js +122 -0
- package/dist/formatters/github-annotation.js.map +1 -0
- package/dist/formatters/pr-comment.d.ts +43 -0
- package/dist/formatters/pr-comment.d.ts.map +1 -0
- package/dist/formatters/pr-comment.js +171 -0
- package/dist/formatters/pr-comment.js.map +1 -0
- package/dist/formatters/sarif.d.ts +104 -0
- package/dist/formatters/sarif.d.ts.map +1 -0
- package/dist/formatters/sarif.js +130 -0
- package/dist/formatters/sarif.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +108 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/base.d.ts +44 -0
- package/dist/integrations/base.d.ts.map +1 -0
- package/dist/integrations/base.js +104 -0
- package/dist/integrations/base.js.map +1 -0
- package/dist/integrations/eslint.d.ts +8 -0
- package/dist/integrations/eslint.d.ts.map +1 -0
- package/dist/integrations/eslint.js +67 -0
- package/dist/integrations/eslint.js.map +1 -0
- package/dist/integrations/formatter.d.ts +35 -0
- package/dist/integrations/formatter.d.ts.map +1 -0
- package/dist/integrations/formatter.js +182 -0
- package/dist/integrations/formatter.js.map +1 -0
- package/dist/integrations/index.d.ts +17 -0
- package/dist/integrations/index.d.ts.map +1 -0
- package/dist/integrations/index.js +25 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/integrations/stylelint.d.ts +8 -0
- package/dist/integrations/stylelint.d.ts.map +1 -0
- package/dist/integrations/stylelint.js +59 -0
- package/dist/integrations/stylelint.js.map +1 -0
- package/dist/integrations/typescript.d.ts +8 -0
- package/dist/integrations/typescript.d.ts.map +1 -0
- package/dist/integrations/typescript.js +92 -0
- package/dist/integrations/typescript.js.map +1 -0
- package/dist/rules/registry.d.ts +83 -0
- package/dist/rules/registry.d.ts.map +1 -0
- package/dist/rules/registry.js +205 -0
- package/dist/rules/registry.js.map +1 -0
- package/dist/scanners/a11y-scanner.d.ts +14 -0
- package/dist/scanners/a11y-scanner.d.ts.map +1 -0
- package/dist/scanners/a11y-scanner.js +781 -0
- package/dist/scanners/a11y-scanner.js.map +1 -0
- package/dist/scanners/component-scanner.d.ts +12 -0
- package/dist/scanners/component-scanner.d.ts.map +1 -0
- package/dist/scanners/component-scanner.js +304 -0
- package/dist/scanners/component-scanner.js.map +1 -0
- package/dist/scanners/cross-file-scanner.d.ts +18 -0
- package/dist/scanners/cross-file-scanner.d.ts.map +1 -0
- package/dist/scanners/cross-file-scanner.js +684 -0
- package/dist/scanners/cross-file-scanner.js.map +1 -0
- package/dist/scanners/hooks-scanner.d.ts +15 -0
- package/dist/scanners/hooks-scanner.d.ts.map +1 -0
- package/dist/scanners/hooks-scanner.js +670 -0
- package/dist/scanners/hooks-scanner.js.map +1 -0
- package/dist/scanners/i18n-scanner.d.ts +13 -0
- package/dist/scanners/i18n-scanner.d.ts.map +1 -0
- package/dist/scanners/i18n-scanner.js +535 -0
- package/dist/scanners/i18n-scanner.js.map +1 -0
- package/dist/scanners/naming-scanner.d.ts +19 -0
- package/dist/scanners/naming-scanner.d.ts.map +1 -0
- package/dist/scanners/naming-scanner.js +746 -0
- package/dist/scanners/naming-scanner.js.map +1 -0
- package/dist/scanners/performance-scanner.d.ts +7 -0
- package/dist/scanners/performance-scanner.d.ts.map +1 -0
- package/dist/scanners/performance-scanner.js +402 -0
- package/dist/scanners/performance-scanner.js.map +1 -0
- package/dist/scanners/platform-scanner.d.ts +15 -0
- package/dist/scanners/platform-scanner.d.ts.map +1 -0
- package/dist/scanners/platform-scanner.js +320 -0
- package/dist/scanners/platform-scanner.js.map +1 -0
- package/dist/scanners/security-scanner.d.ts +7 -0
- package/dist/scanners/security-scanner.d.ts.map +1 -0
- package/dist/scanners/security-scanner.js +349 -0
- package/dist/scanners/security-scanner.js.map +1 -0
- package/dist/scanners/svelte-scanner.d.ts +14 -0
- package/dist/scanners/svelte-scanner.d.ts.map +1 -0
- package/dist/scanners/svelte-scanner.js +228 -0
- package/dist/scanners/svelte-scanner.js.map +1 -0
- package/dist/types.d.ts +343 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/ast-parser.d.ts +21 -0
- package/dist/utils/ast-parser.d.ts.map +1 -0
- package/dist/utils/ast-parser.js +119 -0
- package/dist/utils/ast-parser.js.map +1 -0
- package/dist/utils/baseline.d.ts +89 -0
- package/dist/utils/baseline.d.ts.map +1 -0
- package/dist/utils/baseline.js +156 -0
- package/dist/utils/baseline.js.map +1 -0
- package/dist/utils/ci-generator.d.ts +34 -0
- package/dist/utils/ci-generator.d.ts.map +1 -0
- package/dist/utils/ci-generator.js +194 -0
- package/dist/utils/ci-generator.js.map +1 -0
- package/dist/utils/common.d.ts +8 -0
- package/dist/utils/common.d.ts.map +1 -0
- package/dist/utils/common.js +38 -0
- package/dist/utils/common.js.map +1 -0
- package/dist/utils/concurrent.d.ts +16 -0
- package/dist/utils/concurrent.d.ts.map +1 -0
- package/dist/utils/concurrent.js +49 -0
- package/dist/utils/concurrent.js.map +1 -0
- package/dist/utils/config-loader.d.ts +8 -0
- package/dist/utils/config-loader.d.ts.map +1 -0
- package/dist/utils/config-loader.js +154 -0
- package/dist/utils/config-loader.js.map +1 -0
- package/dist/utils/fix-bot.d.ts +36 -0
- package/dist/utils/fix-bot.d.ts.map +1 -0
- package/dist/utils/fix-bot.js +274 -0
- package/dist/utils/fix-bot.js.map +1 -0
- package/dist/utils/git-hooks.d.ts +55 -0
- package/dist/utils/git-hooks.d.ts.map +1 -0
- package/dist/utils/git-hooks.js +318 -0
- package/dist/utils/git-hooks.js.map +1 -0
- package/dist/utils/history-report.d.ts +72 -0
- package/dist/utils/history-report.d.ts.map +1 -0
- package/dist/utils/history-report.js +144 -0
- package/dist/utils/history-report.js.map +1 -0
- package/dist/utils/init-config.d.ts +23 -0
- package/dist/utils/init-config.d.ts.map +1 -0
- package/dist/utils/init-config.js +146 -0
- package/dist/utils/init-config.js.map +1 -0
- package/dist/utils/pr-publisher.d.ts +64 -0
- package/dist/utils/pr-publisher.d.ts.map +1 -0
- package/dist/utils/pr-publisher.js +265 -0
- package/dist/utils/pr-publisher.js.map +1 -0
- package/dist/utils/project-detector.d.ts +20 -0
- package/dist/utils/project-detector.d.ts.map +1 -0
- package/dist/utils/project-detector.js +342 -0
- package/dist/utils/project-detector.js.map +1 -0
- package/dist/utils/report-uploader.d.ts +35 -0
- package/dist/utils/report-uploader.d.ts.map +1 -0
- package/dist/utils/report-uploader.js +106 -0
- package/dist/utils/report-uploader.js.map +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* RuleEngine — 可插拔规则引擎核心
|
|
4
|
+
*
|
|
5
|
+
* 设计目标:
|
|
6
|
+
* 1. 支持注册/注销规则(插件化)
|
|
7
|
+
* 2. 按 severity 过滤
|
|
8
|
+
* 3. 按 framework/platform/componentLib 条件执行
|
|
9
|
+
* 4. 并行扫描多文件
|
|
10
|
+
* 5. 支持增量扫描(git diff)
|
|
11
|
+
*/
|
|
12
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.RuleEngine = void 0;
|
|
17
|
+
exports.createEngine = createEngine;
|
|
18
|
+
const node_fs_1 = require("node:fs");
|
|
19
|
+
const node_child_process_1 = require("node:child_process");
|
|
20
|
+
const node_readline_1 = require("node:readline");
|
|
21
|
+
const node_path_1 = require("node:path");
|
|
22
|
+
const ast_parser_js_1 = require("../utils/ast-parser.js");
|
|
23
|
+
const project_detector_js_1 = require("../utils/project-detector.js");
|
|
24
|
+
const config_loader_js_1 = require("../utils/config-loader.js");
|
|
25
|
+
const registry_js_1 = require("../rules/registry.js");
|
|
26
|
+
const globby_1 = require("globby");
|
|
27
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
28
|
+
const index_js_1 = require("../integrations/index.js");
|
|
29
|
+
const cache_js_1 = require("./cache.js");
|
|
30
|
+
const history_report_js_1 = require("../utils/history-report.js");
|
|
31
|
+
const formatter_js_1 = require("../integrations/formatter.js");
|
|
32
|
+
const concurrent_js_1 = require("../utils/concurrent.js");
|
|
33
|
+
class RuleEngine {
|
|
34
|
+
registry;
|
|
35
|
+
config = {};
|
|
36
|
+
projectMeta;
|
|
37
|
+
options;
|
|
38
|
+
cache;
|
|
39
|
+
history;
|
|
40
|
+
constructor(options) {
|
|
41
|
+
this.options = options;
|
|
42
|
+
this.config = (0, config_loader_js_1.loadConfig)(options.projectDir, options.configFile);
|
|
43
|
+
this.projectMeta = (0, project_detector_js_1.detectProjectMeta)(options.projectDir, this.config);
|
|
44
|
+
this.registry = (0, registry_js_1.createRegistry)();
|
|
45
|
+
// Phase 3: 从配置加载规则覆盖和自定义规则
|
|
46
|
+
this.loadConfigRules();
|
|
47
|
+
// Phase 5: 初始化智能缓存
|
|
48
|
+
if (options.cache !== false) {
|
|
49
|
+
this.cache = options.cacheInstance ?? new cache_js_1.SmartCache(options.projectDir, options.cacheTtl);
|
|
50
|
+
}
|
|
51
|
+
// Phase 6: 初始化历史报告
|
|
52
|
+
this.history = new history_report_js_1.HistoryReport(options.projectDir);
|
|
53
|
+
}
|
|
54
|
+
/** ── Phase 3: 配置驱动规则加载 ── */
|
|
55
|
+
loadConfigRules() {
|
|
56
|
+
// 1. 加载规则配置覆盖(启用/禁用/severity/params)
|
|
57
|
+
if (this.config.rules && this.config.rules.length > 0) {
|
|
58
|
+
this.registry.loadFromConfig(this.config.rules);
|
|
59
|
+
}
|
|
60
|
+
// 2. 加载自定义规则文件
|
|
61
|
+
if (this.config.customRules && this.config.customRules.length > 0) {
|
|
62
|
+
const paths = this.config.customRules.map((c) => c.path);
|
|
63
|
+
const result = this.registry.loadCustomRules(paths, this.options.projectDir);
|
|
64
|
+
if (result.loaded.length > 0) {
|
|
65
|
+
console.log(picocolors_1.default.blue(`🔌 已加载 ${result.loaded.length} 个自定义规则`));
|
|
66
|
+
}
|
|
67
|
+
if (result.failed.length > 0) {
|
|
68
|
+
console.log(picocolors_1.default.yellow(`⚠️ ${result.failed.length} 个自定义规则加载失败`));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** 注册规则 */
|
|
73
|
+
register(rule) {
|
|
74
|
+
this.registry.register(rule);
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
/** 批量注册 */
|
|
78
|
+
registerAll(rules) {
|
|
79
|
+
this.registry.registerAll(rules);
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
/** 注销规则 */
|
|
83
|
+
unregister(ruleId) {
|
|
84
|
+
this.registry.unregister(ruleId);
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
/** 获取所有已注册规则(应用配置覆盖后) */
|
|
88
|
+
getRules() {
|
|
89
|
+
return this.registry.getActiveRules();
|
|
90
|
+
}
|
|
91
|
+
/** 根据条件过滤规则 */
|
|
92
|
+
filterRules(options) {
|
|
93
|
+
return this.registry.filterRules(options);
|
|
94
|
+
}
|
|
95
|
+
/** 模块名到规则 category 的映射 */
|
|
96
|
+
moduleToCategory(module) {
|
|
97
|
+
const map = {
|
|
98
|
+
a11y: "accessibility",
|
|
99
|
+
naming: "style",
|
|
100
|
+
"cross-file": "architecture",
|
|
101
|
+
};
|
|
102
|
+
return map[module] || module;
|
|
103
|
+
}
|
|
104
|
+
/** 执行扫描 */
|
|
105
|
+
async scan(module) {
|
|
106
|
+
const startTime = Date.now();
|
|
107
|
+
const issues = {
|
|
108
|
+
critical: [],
|
|
109
|
+
warning: [],
|
|
110
|
+
suggestion: [],
|
|
111
|
+
};
|
|
112
|
+
// 先根据 projectMeta 过滤规则,无匹配规则则跳过 glob
|
|
113
|
+
const category = this.moduleToCategory(module);
|
|
114
|
+
const activeRules = this.filterRules({
|
|
115
|
+
category,
|
|
116
|
+
framework: this.projectMeta.framework,
|
|
117
|
+
platform: this.projectMeta.platforms[0],
|
|
118
|
+
componentLib: this.projectMeta.componentLib,
|
|
119
|
+
});
|
|
120
|
+
if (activeRules.length === 0) {
|
|
121
|
+
return {
|
|
122
|
+
module,
|
|
123
|
+
total: 0,
|
|
124
|
+
issues,
|
|
125
|
+
duration: 0,
|
|
126
|
+
filesScanned: 0,
|
|
127
|
+
filesWithIssues: 0,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// 获取扫描文件列表
|
|
131
|
+
const files = await this.getScanFiles();
|
|
132
|
+
let filesWithIssues = 0;
|
|
133
|
+
console.log(picocolors_1.default.blue(`🔍 [${module}] 扫描 ${files.length} 个文件,${activeRules.length} 条规则...`));
|
|
134
|
+
// v2.1.0: 受控并发并行扫描
|
|
135
|
+
const concurrency = this.options.concurrency ?? (0, concurrent_js_1.getDefaultConcurrency)();
|
|
136
|
+
const fileResults = await (0, concurrent_js_1.concurrentMap)(files, concurrency, (file) => this.scanFile(file, activeRules));
|
|
137
|
+
let filesSkipped = 0;
|
|
138
|
+
for (const { issues: fileIssues, skipped } of fileResults) {
|
|
139
|
+
if (skipped) {
|
|
140
|
+
filesSkipped++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (fileIssues.length > 0) {
|
|
144
|
+
filesWithIssues++;
|
|
145
|
+
for (const issue of fileIssues) {
|
|
146
|
+
// severity 过滤
|
|
147
|
+
const severityOrder = { critical: 3, warning: 2, suggestion: 1 };
|
|
148
|
+
const minSev = this.options.minSeverity || "suggestion";
|
|
149
|
+
if (severityOrder[issue.severity] >= severityOrder[minSev]) {
|
|
150
|
+
issues[issue.severity].push(issue);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const total = issues.critical.length + issues.warning.length + issues.suggestion.length;
|
|
156
|
+
const filesScanned = files.length - filesSkipped;
|
|
157
|
+
// Phase 5: 保存缓存并输出统计
|
|
158
|
+
if (this.cache) {
|
|
159
|
+
this.cache.save();
|
|
160
|
+
const stats = this.cache.getStats();
|
|
161
|
+
if (stats.total > 0) {
|
|
162
|
+
console.log(picocolors_1.default.gray(` 💾 缓存: ${stats.valid} 命中 / ${stats.expired} 过期 / ${stats.total} 总计`));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const result = {
|
|
166
|
+
module,
|
|
167
|
+
total,
|
|
168
|
+
issues,
|
|
169
|
+
duration: Date.now() - startTime,
|
|
170
|
+
filesScanned,
|
|
171
|
+
filesWithIssues,
|
|
172
|
+
};
|
|
173
|
+
// Phase 6: 记录扫描历史并输出趋势
|
|
174
|
+
const allIssues = [...issues.critical, ...issues.warning, ...issues.suggestion];
|
|
175
|
+
this.history.record(result, allIssues);
|
|
176
|
+
const trend = this.history.analyze(module, allIssues.map((i) => `${i.file}|${i.ruleId}|${i.line}`));
|
|
177
|
+
if (trend.totalScans > 1) {
|
|
178
|
+
if (trend.newIssues.length > 0) {
|
|
179
|
+
console.log(picocolors_1.default.yellow(` 📈 新增 ${trend.newIssues.length} 个问题(对比上次扫描)`));
|
|
180
|
+
}
|
|
181
|
+
if (trend.fixedIssues.length > 0) {
|
|
182
|
+
console.log(picocolors_1.default.green(` ✅ 已修复 ${trend.fixedIssues.length} 个问题(对比上次扫描)`));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
/** 扫描单个文件(带智能缓存 + 大文件跳过) */
|
|
188
|
+
async scanFile(filePath, rules) {
|
|
189
|
+
try {
|
|
190
|
+
// v2.4.0: 大文件智能跳过
|
|
191
|
+
const threshold = this.options.skipLargeFilesThreshold ?? 512_000;
|
|
192
|
+
if (threshold > 0) {
|
|
193
|
+
try {
|
|
194
|
+
const stats = (0, node_fs_1.statSync)(filePath);
|
|
195
|
+
if (stats.size > threshold) {
|
|
196
|
+
console.log(picocolors_1.default.yellow(` ⚠️ 跳过超大文件: ${filePath} (${(stats.size / 1024).toFixed(1)}KB > ${(threshold / 1024).toFixed(0)}KB)`));
|
|
197
|
+
return { issues: [], skipped: true };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// stat 失败继续尝试读取
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const source = (0, node_fs_1.readFileSync)(filePath, "utf-8");
|
|
205
|
+
// Phase 5: 智能缓存命中检查
|
|
206
|
+
if (this.cache?.isCached(filePath, source)) {
|
|
207
|
+
const cached = this.cache.get(filePath);
|
|
208
|
+
if (cached) {
|
|
209
|
+
return { issues: cached };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const allIssues = [];
|
|
213
|
+
const utils = this.createUtils(filePath, source);
|
|
214
|
+
const context = {
|
|
215
|
+
filePath,
|
|
216
|
+
source,
|
|
217
|
+
config: this.config,
|
|
218
|
+
projectMeta: this.projectMeta,
|
|
219
|
+
utils,
|
|
220
|
+
sharedCache: new Map(),
|
|
221
|
+
};
|
|
222
|
+
for (const rule of rules) {
|
|
223
|
+
try {
|
|
224
|
+
const result = await rule.execute(context);
|
|
225
|
+
// v2.4.0: 为每个 issue 注入 docsUrl
|
|
226
|
+
for (const issue of result) {
|
|
227
|
+
if (rule.docsUrl && !issue.docsUrl) {
|
|
228
|
+
issue.docsUrl = rule.docsUrl;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
allIssues.push(...result);
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
console.error(picocolors_1.default.red(` Rule "${rule.id}" failed on ${filePath}:`), err);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Phase 5: 缓存结果
|
|
238
|
+
this.cache?.set(filePath, source, allIssues);
|
|
239
|
+
return { issues: allIssues };
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
// 文件读取失败,静默跳过
|
|
243
|
+
return { issues: [] };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** 获取扫描文件列表 */
|
|
247
|
+
async getScanFiles() {
|
|
248
|
+
// 增量扫描:git staged / diff 范围 / auto-scope
|
|
249
|
+
if (this.options.staged || this.options.diffRange || this.options.autoScope) {
|
|
250
|
+
const diffFiles = this.options.autoScope
|
|
251
|
+
? this.getAutoScopeFiles()
|
|
252
|
+
: this.getDiffFiles();
|
|
253
|
+
if (diffFiles.length === 0) {
|
|
254
|
+
if (this.options.autoScope) {
|
|
255
|
+
// auto-scope 无结果时回退到全量扫描
|
|
256
|
+
console.log(picocolors_1.default.yellow("⚠️ 未检测到修改文件,回退到全量扫描"));
|
|
257
|
+
}
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
// 过滤出符合扩展名的文件
|
|
261
|
+
const include = this.config.scan?.includeExtensions || [".js", ".ts", ".jsx", ".tsx", ".vue"];
|
|
262
|
+
const filtered = diffFiles.filter((f) => include.some((ext) => f.endsWith(ext)));
|
|
263
|
+
if (this.options.autoScope && filtered.length > 0) {
|
|
264
|
+
console.log(picocolors_1.default.cyan(`🔍 智能扫描范围: ${filtered.length} 个文件`));
|
|
265
|
+
}
|
|
266
|
+
return filtered;
|
|
267
|
+
}
|
|
268
|
+
if (this.options.files && this.options.files.length > 0) {
|
|
269
|
+
return this.options.files;
|
|
270
|
+
}
|
|
271
|
+
const include = this.config.scan?.includeExtensions || [".js", ".ts", ".jsx", ".tsx", ".vue"];
|
|
272
|
+
const exclude = [
|
|
273
|
+
"**/node_modules/**",
|
|
274
|
+
"**/dist/**",
|
|
275
|
+
"**/build/**",
|
|
276
|
+
"**/.git/**",
|
|
277
|
+
"**/coverage/**",
|
|
278
|
+
...(this.options.exclude || []),
|
|
279
|
+
...(this.config.scan?.excludeDirs?.map((d) => `**/${d}/**`) || []),
|
|
280
|
+
];
|
|
281
|
+
const patterns = include.map((ext) => `**/*${ext}`);
|
|
282
|
+
return (0, globby_1.globby)(patterns, {
|
|
283
|
+
cwd: this.options.projectDir,
|
|
284
|
+
ignore: exclude,
|
|
285
|
+
absolute: true,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
/** 通过 git 获取变更文件列表 */
|
|
289
|
+
getDiffFiles() {
|
|
290
|
+
try {
|
|
291
|
+
let cmd;
|
|
292
|
+
if (this.options.staged) {
|
|
293
|
+
cmd = "git diff --cached --name-only --diff-filter=ACM";
|
|
294
|
+
}
|
|
295
|
+
else if (this.options.diffRange) {
|
|
296
|
+
cmd = `git diff --name-only --diff-filter=ACM ${this.options.diffRange}`;
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
const output = (0, node_child_process_1.execSync)(cmd, {
|
|
302
|
+
cwd: this.options.projectDir,
|
|
303
|
+
encoding: "utf-8",
|
|
304
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
305
|
+
});
|
|
306
|
+
return output
|
|
307
|
+
.trim()
|
|
308
|
+
.split("\n")
|
|
309
|
+
.filter(Boolean)
|
|
310
|
+
.map((f) => (0, node_path_1.resolve)(this.options.projectDir, f));
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/** 智能推断扫描范围:未提交修改 → 最近 5 次提交 → 全量 */
|
|
317
|
+
getAutoScopeFiles() {
|
|
318
|
+
try {
|
|
319
|
+
const files = new Set();
|
|
320
|
+
// 1. 未提交的修改(unstaged + staged)
|
|
321
|
+
for (const cmd of [
|
|
322
|
+
"git diff --name-only --diff-filter=ACM",
|
|
323
|
+
"git diff --cached --name-only --diff-filter=ACM",
|
|
324
|
+
]) {
|
|
325
|
+
try {
|
|
326
|
+
const output = (0, node_child_process_1.execSync)(cmd, {
|
|
327
|
+
cwd: this.options.projectDir,
|
|
328
|
+
encoding: "utf-8",
|
|
329
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
330
|
+
});
|
|
331
|
+
output
|
|
332
|
+
.trim()
|
|
333
|
+
.split("\n")
|
|
334
|
+
.filter(Boolean)
|
|
335
|
+
.forEach((f) => files.add((0, node_path_1.resolve)(this.options.projectDir, f)));
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
// 忽略单条命令失败
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// 2. 无未提交修改时,回退到最近 5 次提交
|
|
342
|
+
if (files.size === 0) {
|
|
343
|
+
try {
|
|
344
|
+
const output = (0, node_child_process_1.execSync)("git diff --name-only --diff-filter=ACM HEAD~5...HEAD", {
|
|
345
|
+
cwd: this.options.projectDir,
|
|
346
|
+
encoding: "utf-8",
|
|
347
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
348
|
+
});
|
|
349
|
+
output
|
|
350
|
+
.trim()
|
|
351
|
+
.split("\n")
|
|
352
|
+
.filter(Boolean)
|
|
353
|
+
.forEach((f) => files.add((0, node_path_1.resolve)(this.options.projectDir, f)));
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// 可能不足 5 次提交,忽略
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return Array.from(files);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/** 创建 RuleUtils */
|
|
366
|
+
createUtils(filePath, source) {
|
|
367
|
+
const lineOffsets = this.computeLineOffsets(source);
|
|
368
|
+
const cache = this.cache;
|
|
369
|
+
return {
|
|
370
|
+
// v2.1.0: parseAST 注入 AST 缓存,同一文件未变更时跳过重新解析
|
|
371
|
+
parseAST: (src, options) => {
|
|
372
|
+
if (cache && src === source) {
|
|
373
|
+
const cached = cache.getAst(filePath, source);
|
|
374
|
+
if (cached)
|
|
375
|
+
return cached;
|
|
376
|
+
}
|
|
377
|
+
const ast = (0, ast_parser_js_1.parseAST)(src, options);
|
|
378
|
+
if (cache && ast && src === source) {
|
|
379
|
+
cache.setAst(filePath, source, ast);
|
|
380
|
+
}
|
|
381
|
+
return ast;
|
|
382
|
+
},
|
|
383
|
+
getImports: (ast) => (0, ast_parser_js_1.getImports)(ast),
|
|
384
|
+
reportPosition: (offset) => {
|
|
385
|
+
let line = 1;
|
|
386
|
+
let column = 1;
|
|
387
|
+
for (let i = 0; i < lineOffsets.length; i++) {
|
|
388
|
+
if (offset < lineOffsets[i]) {
|
|
389
|
+
line = i;
|
|
390
|
+
column = offset - (lineOffsets[i - 1] || 0) + 1;
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
if (i === lineOffsets.length - 1) {
|
|
394
|
+
line = lineOffsets.length;
|
|
395
|
+
column = offset - lineOffsets[i] + 1;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return { line, column };
|
|
399
|
+
},
|
|
400
|
+
getSourceSnippet: (start, end) => {
|
|
401
|
+
return source.slice(start, end);
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/** 计算每行起始偏移 */
|
|
406
|
+
computeLineOffsets(source) {
|
|
407
|
+
const offsets = [0];
|
|
408
|
+
for (let i = 0; i < source.length; i++) {
|
|
409
|
+
if (source[i] === "\n") {
|
|
410
|
+
offsets.push(i + 1);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return offsets;
|
|
414
|
+
}
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
// 自动修复
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
/**
|
|
419
|
+
* 应用所有可修复的问题
|
|
420
|
+
* @param issues 包含 fix 字段的 Issue 列表
|
|
421
|
+
* @returns 修复统计(dryRun 模式下 filesModified 为空,fixedCount 为预览数量)
|
|
422
|
+
*/
|
|
423
|
+
applyFixes(issues) {
|
|
424
|
+
const dryRun = this.options.dryRun;
|
|
425
|
+
const interactive = this.options.interactive;
|
|
426
|
+
let fixedCount = 0;
|
|
427
|
+
let skippedByUser = 0;
|
|
428
|
+
const filesModified = [];
|
|
429
|
+
const errors = [];
|
|
430
|
+
const previews = [];
|
|
431
|
+
// 按文件分组
|
|
432
|
+
const byFile = new Map();
|
|
433
|
+
for (const issue of issues) {
|
|
434
|
+
if (!issue.fix)
|
|
435
|
+
continue;
|
|
436
|
+
const list = byFile.get(issue.file) || [];
|
|
437
|
+
list.push(issue);
|
|
438
|
+
byFile.set(issue.file, list);
|
|
439
|
+
}
|
|
440
|
+
for (const [filePath, fileIssues] of byFile) {
|
|
441
|
+
try {
|
|
442
|
+
let source = (0, node_fs_1.readFileSync)(filePath, "utf-8");
|
|
443
|
+
const originalSource = source;
|
|
444
|
+
// 按行号倒序排列,从文件末尾开始修复,避免行号偏移
|
|
445
|
+
const sorted = [...fileIssues].sort((a, b) => {
|
|
446
|
+
const lineDiff = (b.fix.start.line || 0) - (a.fix.start.line || 0);
|
|
447
|
+
if (lineDiff !== 0)
|
|
448
|
+
return lineDiff;
|
|
449
|
+
return (b.fix.start.column || 0) - (a.fix.start.column || 0);
|
|
450
|
+
});
|
|
451
|
+
for (const issue of sorted) {
|
|
452
|
+
const fix = issue.fix;
|
|
453
|
+
const confidence = fix.confidence ?? "high";
|
|
454
|
+
const patched = this.applySingleFix(source, fix);
|
|
455
|
+
if (dryRun) {
|
|
456
|
+
// 生成 diff 预览
|
|
457
|
+
const diff = this.makeDiffPreview(source, patched, fix);
|
|
458
|
+
previews.push({
|
|
459
|
+
file: filePath,
|
|
460
|
+
ruleId: issue.ruleId,
|
|
461
|
+
title: issue.title,
|
|
462
|
+
diff,
|
|
463
|
+
});
|
|
464
|
+
fixedCount++;
|
|
465
|
+
source = patched;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (interactive) {
|
|
469
|
+
// v2.4.0: 交互式修复模式 — 逐条确认
|
|
470
|
+
const shouldApply = this.promptForFix(issue, confidence, source, patched);
|
|
471
|
+
if (shouldApply) {
|
|
472
|
+
const before = source;
|
|
473
|
+
source = patched;
|
|
474
|
+
if (source !== before) {
|
|
475
|
+
fixedCount++;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
skippedByUser++;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
// 自动修复:低置信度自动跳过
|
|
484
|
+
if (confidence === "low") {
|
|
485
|
+
skippedByUser++;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const before = source;
|
|
489
|
+
source = patched;
|
|
490
|
+
if (source !== before) {
|
|
491
|
+
fixedCount++;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (source !== originalSource) {
|
|
496
|
+
if (!dryRun) {
|
|
497
|
+
(0, node_fs_1.writeFileSync)(filePath, source, "utf-8");
|
|
498
|
+
}
|
|
499
|
+
filesModified.push(filePath);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
catch (err) {
|
|
503
|
+
errors.push(`修复 ${filePath} 失败: ${err}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const result = { fixedCount, filesModified, errors, skippedByUser };
|
|
507
|
+
if (dryRun) {
|
|
508
|
+
result.previews = previews;
|
|
509
|
+
}
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* v2.4.0: 交互式修复 — 向用户展示 diff 并询问是否应用
|
|
514
|
+
* 同步阻塞式输入(基于 readline),适用于 CLI 场景
|
|
515
|
+
*/
|
|
516
|
+
promptForFix(issue, confidence, original, patched) {
|
|
517
|
+
const diff = this.makeDiffPreview(original, patched, issue.fix);
|
|
518
|
+
const confidenceIcon = confidence === "high" ? picocolors_1.default.green("●") : confidence === "medium" ? picocolors_1.default.yellow("●") : picocolors_1.default.red("●");
|
|
519
|
+
const confidenceLabel = confidence === "high" ? "高置信度" : confidence === "medium" ? "中置信度" : "低置信度";
|
|
520
|
+
console.log(picocolors_1.default.cyan(`\n 📄 ${issue.file}:${issue.line}`));
|
|
521
|
+
console.log(picocolors_1.default.yellow(` [${issue.ruleId}] ${issue.title}`));
|
|
522
|
+
console.log(picocolors_1.default.gray(` 置信度: ${confidenceIcon} ${confidenceLabel}`));
|
|
523
|
+
if (issue.fix?.description) {
|
|
524
|
+
console.log(picocolors_1.default.gray(` 说明: ${issue.fix.description}`));
|
|
525
|
+
}
|
|
526
|
+
console.log(diff);
|
|
527
|
+
console.log(picocolors_1.default.gray(" 选项: [y] 应用 [n] 跳过 [a] 全部应用 [q] 退出"));
|
|
528
|
+
// 使用同步 readline 读取用户输入
|
|
529
|
+
const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
530
|
+
try {
|
|
531
|
+
// 简单同步读取(适用于 Node.js CLI)
|
|
532
|
+
const answer = this.readSyncLine(rl);
|
|
533
|
+
const trimmed = answer.trim().toLowerCase();
|
|
534
|
+
if (trimmed === "a") {
|
|
535
|
+
// 全部应用:关闭交互模式,后续自动应用
|
|
536
|
+
this.options.interactive = false;
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
if (trimmed === "q") {
|
|
540
|
+
console.log(picocolors_1.default.gray(" 已退出交互式修复"));
|
|
541
|
+
process.exit(0);
|
|
542
|
+
}
|
|
543
|
+
return trimmed === "y" || trimmed === "yes" || trimmed === "";
|
|
544
|
+
}
|
|
545
|
+
finally {
|
|
546
|
+
rl.close();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/** 同步读取一行输入 */
|
|
550
|
+
readSyncLine(rl) {
|
|
551
|
+
const { stdin, stdout } = process;
|
|
552
|
+
stdin.setRawMode?.(true);
|
|
553
|
+
stdin.resume();
|
|
554
|
+
let result = "";
|
|
555
|
+
const buf = Buffer.alloc(1);
|
|
556
|
+
while (true) {
|
|
557
|
+
const bytesRead = stdin.readSync ? stdin.readSync(buf) : 0;
|
|
558
|
+
if (bytesRead === 0)
|
|
559
|
+
continue;
|
|
560
|
+
const char = buf.toString("utf8");
|
|
561
|
+
if (char === "\n" || char === "\r")
|
|
562
|
+
break;
|
|
563
|
+
if (char === "")
|
|
564
|
+
process.exit(0); // Ctrl+C
|
|
565
|
+
result += char;
|
|
566
|
+
stdout.write(char);
|
|
567
|
+
}
|
|
568
|
+
stdout.write("\n");
|
|
569
|
+
stdin.setRawMode?.(false);
|
|
570
|
+
stdin.pause();
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
/** 对单文件应用单个修复 */
|
|
574
|
+
applySingleFix(source, fix) {
|
|
575
|
+
const lines = source.split("\n");
|
|
576
|
+
const { line: startLine, column: startCol } = fix.start;
|
|
577
|
+
const { line: endLine, column: endCol } = fix.end;
|
|
578
|
+
// 单行修复
|
|
579
|
+
if (startLine === endLine) {
|
|
580
|
+
const idx = startLine - 1;
|
|
581
|
+
if (idx < 0 || idx >= lines.length)
|
|
582
|
+
return source;
|
|
583
|
+
const targetLine = lines[idx];
|
|
584
|
+
const before = targetLine.slice(0, Math.max(0, startCol - 1));
|
|
585
|
+
const after = targetLine.slice(Math.max(0, endCol - 1));
|
|
586
|
+
lines[idx] = before + fix.text + after;
|
|
587
|
+
return lines.join("\n");
|
|
588
|
+
}
|
|
589
|
+
// 多行修复:替换从 start 到 end 的所有内容
|
|
590
|
+
const startIdx = startLine - 1;
|
|
591
|
+
const endIdx = endLine - 1;
|
|
592
|
+
if (startIdx < 0 || endIdx >= lines.length)
|
|
593
|
+
return source;
|
|
594
|
+
const before = lines[startIdx].slice(0, Math.max(0, startCol - 1));
|
|
595
|
+
const after = lines[endIdx].slice(Math.max(0, endCol - 1));
|
|
596
|
+
const newLines = fix.text.split("\n");
|
|
597
|
+
// 合并首尾
|
|
598
|
+
newLines[0] = before + newLines[0];
|
|
599
|
+
newLines[newLines.length - 1] = newLines[newLines.length - 1] + after;
|
|
600
|
+
// 替换行范围
|
|
601
|
+
lines.splice(startIdx, endIdx - startIdx + 1, ...newLines);
|
|
602
|
+
return lines.join("\n");
|
|
603
|
+
}
|
|
604
|
+
/** 生成 diff 预览(dry-run 模式) */
|
|
605
|
+
makeDiffPreview(original, patched, fix) {
|
|
606
|
+
const origLines = original.split("\n");
|
|
607
|
+
const patchedLines = patched.split("\n");
|
|
608
|
+
const { line: startLine } = fix.start;
|
|
609
|
+
const { line: endLine } = fix.end;
|
|
610
|
+
// 展示变更前后的上下文(前后各2行)
|
|
611
|
+
const contextBefore = Math.max(0, startLine - 3);
|
|
612
|
+
const contextAfter = Math.min(origLines.length, endLine + 2);
|
|
613
|
+
const lines = [];
|
|
614
|
+
for (let i = contextBefore; i < contextAfter; i++) {
|
|
615
|
+
const orig = origLines[i] || "";
|
|
616
|
+
const patch = patchedLines[i] || "";
|
|
617
|
+
if (i >= startLine - 1 && i < endLine) {
|
|
618
|
+
lines.push(picocolors_1.default.red(`- ${orig}`));
|
|
619
|
+
}
|
|
620
|
+
if (i >= startLine - 1 && i < startLine - 1 + fix.text.split("\n").length) {
|
|
621
|
+
const patchLines = patch.split("\n");
|
|
622
|
+
const idx = i - (startLine - 1);
|
|
623
|
+
if (patchLines[idx]) {
|
|
624
|
+
lines.push(picocolors_1.default.green(`+ ${patchLines[idx]}`));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (i < startLine - 1 || i >= endLine) {
|
|
628
|
+
lines.push(picocolors_1.default.gray(` ${orig}`));
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return lines.join("\n");
|
|
632
|
+
}
|
|
633
|
+
/** 清理过期缓存 */
|
|
634
|
+
gcCache() {
|
|
635
|
+
return this.cache?.gc() ?? 0;
|
|
636
|
+
}
|
|
637
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
638
|
+
// Phase 5/6: 代码格式化(对被扫描项目)
|
|
639
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
640
|
+
/**
|
|
641
|
+
* 格式化被扫描项目的代码
|
|
642
|
+
* 自动检测 Biome / Prettier,使用项目已有配置或生成默认配置
|
|
643
|
+
* @param files 指定文件列表(undefined 则格式化全部)
|
|
644
|
+
*/
|
|
645
|
+
format(files) {
|
|
646
|
+
return (0, formatter_js_1.runFormat)(this.options.projectDir, files);
|
|
647
|
+
}
|
|
648
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
649
|
+
// Issue 聚类 (Phase 2: 智能化)
|
|
650
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
651
|
+
/**
|
|
652
|
+
* 将相似 Issue 聚类为聚合 Issue
|
|
653
|
+
* 按 (file, ruleId) 分组,同一文件同一规则的多个 Issue 合并为一个
|
|
654
|
+
*/
|
|
655
|
+
clusterIssues(issues) {
|
|
656
|
+
const groups = new Map();
|
|
657
|
+
for (const issue of issues) {
|
|
658
|
+
const key = `${issue.file}|${issue.ruleId}`;
|
|
659
|
+
const list = groups.get(key) || [];
|
|
660
|
+
list.push(issue);
|
|
661
|
+
groups.set(key, list);
|
|
662
|
+
}
|
|
663
|
+
const clustered = [];
|
|
664
|
+
for (const [, groupIssues] of groups) {
|
|
665
|
+
if (groupIssues.length === 1) {
|
|
666
|
+
clustered.push(groupIssues[0]);
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
// 按行号排序,取第一个作为代表
|
|
670
|
+
const sorted = [...groupIssues].sort((a, b) => a.line - b.line || a.column - b.column);
|
|
671
|
+
const representative = sorted[0];
|
|
672
|
+
const allLines = sorted.map((i) => i.line);
|
|
673
|
+
clustered.push({
|
|
674
|
+
...representative,
|
|
675
|
+
title: `${representative.title} (×${groupIssues.length})`,
|
|
676
|
+
description: `${representative.description}\n\n聚类详情:在 ${groupIssues.length} 处发现同类问题(行: ${allLines.join(", ")})`,
|
|
677
|
+
meta: {
|
|
678
|
+
...representative.meta,
|
|
679
|
+
clusterCount: groupIssues.length,
|
|
680
|
+
clusteredLines: allLines,
|
|
681
|
+
clusteredRuleId: representative.ruleId,
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
return clustered;
|
|
686
|
+
}
|
|
687
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
688
|
+
// Phase 4: 外部工具集成 (ESLint / TypeScript / Stylelint)
|
|
689
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
690
|
+
/**
|
|
691
|
+
* 运行外部工具集成检查
|
|
692
|
+
* 自动检测项目中可用的工具(ESLint / TypeScript / Stylelint)并执行
|
|
693
|
+
*/
|
|
694
|
+
runExternal(tools) {
|
|
695
|
+
const targetTools = tools || this.getDefaultExternalTools();
|
|
696
|
+
if (targetTools.length === 0) {
|
|
697
|
+
return [];
|
|
698
|
+
}
|
|
699
|
+
console.log(picocolors_1.default.cyan("🔌 运行外部工具集成..."));
|
|
700
|
+
// 获取当前扫描文件列表(用于增量模式)
|
|
701
|
+
const scanFiles = this.options.staged || this.options.diffRange ? this.getDiffFiles() : undefined;
|
|
702
|
+
return (0, index_js_1.runAllExternalTools)(this.options.projectDir, targetTools, scanFiles);
|
|
703
|
+
}
|
|
704
|
+
/** 获取默认的外部工具列表 */
|
|
705
|
+
getDefaultExternalTools() {
|
|
706
|
+
// 动态导入避免循环依赖
|
|
707
|
+
const { allExternalTools } = require("../integrations/index.js");
|
|
708
|
+
return allExternalTools;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
exports.RuleEngine = RuleEngine;
|
|
712
|
+
/** 创建默认引擎实例 */
|
|
713
|
+
function createEngine(options) {
|
|
714
|
+
return new RuleEngine(options);
|
|
715
|
+
}
|
|
716
|
+
//# sourceMappingURL=rule-engine.js.map
|