auto-cr-rules 2.0.75 → 2.0.78

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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noSwallowedErrors = exports.noDeepRelativeImports = exports.builtinRules = exports.createRuleContext = exports.toRule = exports.isRule = exports.defineRule = exports.RuleSeverity = void 0;
3
+ exports.noSwallowedErrors = exports.noCircularDependencies = exports.noDeepRelativeImports = exports.builtinRules = exports.createRuleContext = exports.toRule = exports.isRule = exports.defineRule = exports.RuleSeverity = void 0;
4
4
  var types_1 = require("./types");
5
5
  Object.defineProperty(exports, "RuleSeverity", { enumerable: true, get: function () { return types_1.RuleSeverity; } });
6
6
  var types_2 = require("./types");
@@ -12,4 +12,5 @@ Object.defineProperty(exports, "createRuleContext", { enumerable: true, get: fun
12
12
  var rules_1 = require("./rules");
13
13
  Object.defineProperty(exports, "builtinRules", { enumerable: true, get: function () { return rules_1.builtinRules; } });
14
14
  Object.defineProperty(exports, "noDeepRelativeImports", { enumerable: true, get: function () { return rules_1.noDeepRelativeImports; } });
15
+ Object.defineProperty(exports, "noCircularDependencies", { enumerable: true, get: function () { return rules_1.noCircularDependencies; } });
15
16
  Object.defineProperty(exports, "noSwallowedErrors", { enumerable: true, get: function () { return rules_1.noSwallowedErrors; } });
package/dist/messages.js CHANGED
@@ -8,6 +8,10 @@ var ruleTranslations = {
8
8
  return "\u5BFC\u5165\u8DEF\u5F84 \"".concat(value, "\"\uFF0C\u4E0D\u80FD\u8D85\u8FC7\u6700\u5927\u5C42\u7EA7").concat(maxDepth);
9
9
  },
10
10
  swallowedError: function () { return '捕获到的异常未被重新抛出或记录,可能导致问题被静默吞噬。'; },
11
+ circularDependency: function (_a) {
12
+ var chain = _a.chain;
13
+ return "\u68C0\u6D4B\u5230\u5FAA\u73AF\u4F9D\u8D56: ".concat(chain);
14
+ },
11
15
  },
12
16
  en: {
13
17
  noDeepRelativeImports: function (_a) {
@@ -15,6 +19,10 @@ var ruleTranslations = {
15
19
  return "Import path \"".concat(value, "\" must not exceed max depth ").concat(maxDepth);
16
20
  },
17
21
  swallowedError: function () { return 'Caught exception is neither rethrown nor logged; potential swallowed error detected.'; },
22
+ circularDependency: function (_a) {
23
+ var chain = _a.chain;
24
+ return "Circular dependency detected: ".concat(chain);
25
+ },
18
26
  },
19
27
  };
20
28
  Object.values(ruleTranslations).forEach(function (messages) { return Object.freeze(messages); });
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noSwallowedErrors = exports.noDeepRelativeImports = exports.builtinRules = void 0;
3
+ exports.noSwallowedErrors = exports.noCircularDependencies = exports.noDeepRelativeImports = exports.builtinRules = void 0;
4
4
  var noDeepRelativeImports_1 = require("./noDeepRelativeImports");
5
5
  Object.defineProperty(exports, "noDeepRelativeImports", { enumerable: true, get: function () { return noDeepRelativeImports_1.noDeepRelativeImports; } });
6
+ var noCircularDependencies_1 = require("./noCircularDependencies");
7
+ Object.defineProperty(exports, "noCircularDependencies", { enumerable: true, get: function () { return noCircularDependencies_1.noCircularDependencies; } });
6
8
  var noSwallowedErrors_1 = require("./noSwallowedErrors");
7
9
  Object.defineProperty(exports, "noSwallowedErrors", { enumerable: true, get: function () { return noSwallowedErrors_1.noSwallowedErrors; } });
8
- exports.builtinRules = [noDeepRelativeImports_1.noDeepRelativeImports, noSwallowedErrors_1.noSwallowedErrors];
10
+ exports.builtinRules = [noDeepRelativeImports_1.noDeepRelativeImports, noCircularDependencies_1.noCircularDependencies, noSwallowedErrors_1.noSwallowedErrors];
@@ -0,0 +1,404 @@
1
+ "use strict";
2
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
3
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
4
+ if (ar || !(i in from)) {
5
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
6
+ ar[i] = from[i];
7
+ }
8
+ }
9
+ return to.concat(ar || Array.prototype.slice.call(from));
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.noCircularDependencies = void 0;
16
+ var fs_1 = __importDefault(require("fs"));
17
+ var path_1 = __importDefault(require("path"));
18
+ var types_1 = require("../types");
19
+ /**
20
+ * 检测相对路径形成的循环依赖:
21
+ * - 仅解析 .ts/.tsx/.js/.jsx/.mjs/.cjs;
22
+ * - 从当前文件的 import 出发,沿依赖图寻找回到自身的路径;
23
+ * - 输出完整环路链路,便于定位循环发生的文件。
24
+ */
25
+ var SUPPORTED_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
26
+ // 避免在大型仓库里构图过深或过大导致卡顿。
27
+ var MAX_GRAPH_NODES = 2000;
28
+ var MAX_GRAPH_DEPTH = 80;
29
+ // 跨文件缓存解析结果,减少重复 IO 与正则扫描。
30
+ var resolvedImportCache = new Map();
31
+ // 记录已上报的环路(做规范化),避免同一环路重复报错。
32
+ var reportedCycles = new Set();
33
+ exports.noCircularDependencies = (0, types_1.defineRule)('no-circular-dependencies', { tag: 'base', severity: types_1.RuleSeverity.Warning }, function (_a) {
34
+ var _b, _c;
35
+ var ast = _a.ast, filePath = _a.filePath, helpers = _a.helpers, language = _a.language, messages = _a.messages, source = _a.source;
36
+ var origin = path_1.default.resolve(filePath);
37
+ var root = resolveProjectRoot(origin);
38
+ var moduleStart = (_c = (_b = ast.span) === null || _b === void 0 ? void 0 : _b.start) !== null && _c !== void 0 ? _c : 0;
39
+ var lineIndex = buildLineIndex(source);
40
+ // 先用 SWC 提供的 import 列表初始化当前文件的依赖,保证准确性与性能。
41
+ resolvedImportCache.set(origin, resolveFromReferences(origin, helpers.imports, root));
42
+ for (var _i = 0, _d = helpers.imports; _i < _d.length; _i++) {
43
+ var reference = _d[_i];
44
+ if (!helpers.isRelativePath(reference.value)) {
45
+ continue;
46
+ }
47
+ var target = resolveImportFile(origin, reference.value, root);
48
+ if (!target) {
49
+ continue;
50
+ }
51
+ // 从目标模块回溯,如果能再次回到 origin,即存在环路。
52
+ var pathToOrigin = findPathToOrigin(target, origin, root);
53
+ if (!pathToOrigin) {
54
+ continue;
55
+ }
56
+ var cycle = __spreadArray([origin], pathToOrigin, true);
57
+ var cycleKey = buildCycleKey(cycle);
58
+ if (reportedCycles.has(cycleKey)) {
59
+ continue;
60
+ }
61
+ reportedCycles.add(cycleKey);
62
+ // 统一输出相对路径,便于直接定位到仓库内文件。
63
+ var displayChain = formatCycle(cycle, root);
64
+ var description = messages.circularDependency({ chain: displayChain });
65
+ var suggestions = language === 'zh'
66
+ ? [
67
+ { text: '考虑拆分模块,避免相互依赖。' },
68
+ { text: '抽取共享逻辑到独立模块以打破循环。' },
69
+ ]
70
+ : [
71
+ { text: 'Split modules to avoid mutual dependencies.' },
72
+ { text: 'Extract shared logic into a dedicated module to break the cycle.' },
73
+ ];
74
+ // 优先使用 SWC 的 span 计算行号,作为高可信位置;失败时再用文本匹配兜底。
75
+ var computedLine = reference.span
76
+ ? resolveLine(lineIndex, bytePosToCharIndex(source, moduleStart, reference.span.start))
77
+ : undefined;
78
+ var fallbackLine = findImportLine(source, reference.value);
79
+ var line = selectLineNumber(computedLine, fallbackLine);
80
+ helpers.reportViolation({
81
+ description: description,
82
+ code: displayChain,
83
+ suggestions: suggestions,
84
+ span: reference.span,
85
+ line: line,
86
+ }, reference.span);
87
+ }
88
+ });
89
+ // 选择项目根目录:优先使用当前工作目录,找不到则向上寻找最近的 package.json。
90
+ var resolveProjectRoot = function (filePath) {
91
+ var cwd = path_1.default.resolve(process.cwd());
92
+ if (isWithinRoot(filePath, cwd)) {
93
+ return cwd;
94
+ }
95
+ var current = path_1.default.dirname(filePath);
96
+ var last = '';
97
+ while (current !== last) {
98
+ if (fs_1.default.existsSync(path_1.default.join(current, 'package.json'))) {
99
+ return current;
100
+ }
101
+ last = current;
102
+ current = path_1.default.dirname(current);
103
+ }
104
+ return cwd;
105
+ };
106
+ // 防止解析路径逃逸到仓库外,避免跨项目误报。
107
+ var isWithinRoot = function (filePath, root) {
108
+ var relative = path_1.default.relative(root, filePath);
109
+ return relative === '' || (!relative.startsWith('..') && !path_1.default.isAbsolute(relative));
110
+ };
111
+ // 将当前文件的 import 列表解析为真实文件路径(只处理相对路径)。
112
+ var resolveFromReferences = function (origin, references, root) {
113
+ var resolved = new Set();
114
+ for (var _i = 0, references_1 = references; _i < references_1.length; _i++) {
115
+ var reference = references_1[_i];
116
+ if (!reference.value.startsWith('.')) {
117
+ continue;
118
+ }
119
+ var target = resolveImportFile(origin, reference.value, root);
120
+ if (target) {
121
+ resolved.add(target);
122
+ }
123
+ }
124
+ return Array.from(resolved);
125
+ };
126
+ // DFS 搜索依赖图中是否存在一条从 start 回到 origin 的路径。
127
+ // 通过节点数与深度上限,避免超大项目中搜索失控。
128
+ var findPathToOrigin = function (start, origin, root) {
129
+ var nodesVisited = 0;
130
+ var visiting = new Set();
131
+ var deadEnds = new Set();
132
+ var walk = function (current, depth) {
133
+ if (depth > MAX_GRAPH_DEPTH) {
134
+ return null;
135
+ }
136
+ if (current === origin) {
137
+ return [origin];
138
+ }
139
+ if (deadEnds.has(current)) {
140
+ return null;
141
+ }
142
+ if (visiting.has(current)) {
143
+ return null;
144
+ }
145
+ nodesVisited += 1;
146
+ if (nodesVisited > MAX_GRAPH_NODES) {
147
+ return null;
148
+ }
149
+ visiting.add(current);
150
+ var neighbors = getResolvedImports(current, root);
151
+ for (var _i = 0, neighbors_1 = neighbors; _i < neighbors_1.length; _i++) {
152
+ var next = neighbors_1[_i];
153
+ var result = walk(next, depth + 1);
154
+ if (result) {
155
+ visiting.delete(current);
156
+ return __spreadArray([current], result, true);
157
+ }
158
+ }
159
+ visiting.delete(current);
160
+ deadEnds.add(current);
161
+ return null;
162
+ };
163
+ return walk(start, 0);
164
+ };
165
+ // 读取文件并通过简单正则抽取 import/require/export-from。
166
+ // 这里不重新解析 AST,成本低但可能漏掉非常规写法。
167
+ var getResolvedImports = function (filePath, root) {
168
+ var cached = resolvedImportCache.get(filePath);
169
+ if (cached) {
170
+ return cached;
171
+ }
172
+ var source = readFileSafe(filePath);
173
+ if (!source) {
174
+ resolvedImportCache.set(filePath, []);
175
+ return [];
176
+ }
177
+ var resolved = new Set();
178
+ var specifiers = extractImportSpecifiers(source);
179
+ for (var _i = 0, specifiers_1 = specifiers; _i < specifiers_1.length; _i++) {
180
+ var spec = specifiers_1[_i];
181
+ if (!spec.startsWith('.')) {
182
+ continue;
183
+ }
184
+ var target = resolveImportFile(filePath, spec, root);
185
+ if (target) {
186
+ resolved.add(target);
187
+ }
188
+ }
189
+ var list = Array.from(resolved);
190
+ resolvedImportCache.set(filePath, list);
191
+ return list;
192
+ };
193
+ var readFileSafe = function (filePath) {
194
+ try {
195
+ return fs_1.default.readFileSync(filePath, 'utf-8');
196
+ }
197
+ catch (_a) {
198
+ return null;
199
+ }
200
+ };
201
+ // 用正则匹配常见的导入写法,覆盖 import / dynamic import / require / export-from。
202
+ var extractImportSpecifiers = function (source) {
203
+ var results = [];
204
+ var patterns = [
205
+ /import\s+(?:[^'"]+\s+from\s+)?['"]([^'"]+)['"]/g,
206
+ /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
207
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
208
+ /export\s+[^'"]*from\s+['"]([^'"]+)['"]/g,
209
+ ];
210
+ for (var _i = 0, patterns_1 = patterns; _i < patterns_1.length; _i++) {
211
+ var pattern = patterns_1[_i];
212
+ var match = void 0;
213
+ while ((match = pattern.exec(source)) !== null) {
214
+ results.push(match[1]);
215
+ }
216
+ }
217
+ return results;
218
+ };
219
+ // 解析相对路径到真实文件:支持自动补全扩展名与目录 index。
220
+ // 并确保解析结果在项目根目录内。
221
+ var resolveImportFile = function (fromFile, specifier, root) {
222
+ if (!specifier.startsWith('.')) {
223
+ return null;
224
+ }
225
+ var cleaned = specifier.split(/[?#]/)[0];
226
+ var basePath = path_1.default.resolve(path_1.default.dirname(fromFile), cleaned);
227
+ var resolved = resolveFile(basePath) ||
228
+ resolveWithExtensions(basePath) ||
229
+ resolveFromDirectory(basePath);
230
+ if (!resolved) {
231
+ return null;
232
+ }
233
+ if (!isWithinRoot(resolved, root)) {
234
+ return null;
235
+ }
236
+ if (resolved.endsWith('.d.ts')) {
237
+ return null;
238
+ }
239
+ return resolved;
240
+ };
241
+ var resolveFile = function (candidate) {
242
+ if (!fs_1.default.existsSync(candidate)) {
243
+ return null;
244
+ }
245
+ try {
246
+ if (fs_1.default.statSync(candidate).isFile()) {
247
+ return candidate;
248
+ }
249
+ }
250
+ catch (_a) {
251
+ return null;
252
+ }
253
+ return null;
254
+ };
255
+ var resolveWithExtensions = function (basePath) {
256
+ var ext = path_1.default.extname(basePath);
257
+ if (ext && SUPPORTED_EXTENSIONS.includes(ext)) {
258
+ return resolveFile(basePath);
259
+ }
260
+ for (var _i = 0, SUPPORTED_EXTENSIONS_1 = SUPPORTED_EXTENSIONS; _i < SUPPORTED_EXTENSIONS_1.length; _i++) {
261
+ var extension = SUPPORTED_EXTENSIONS_1[_i];
262
+ var candidate = "".concat(basePath).concat(extension);
263
+ var resolved = resolveFile(candidate);
264
+ if (resolved) {
265
+ return resolved;
266
+ }
267
+ }
268
+ return null;
269
+ };
270
+ var resolveFromDirectory = function (basePath) {
271
+ if (!fs_1.default.existsSync(basePath)) {
272
+ return null;
273
+ }
274
+ try {
275
+ if (!fs_1.default.statSync(basePath).isDirectory()) {
276
+ return null;
277
+ }
278
+ }
279
+ catch (_a) {
280
+ return null;
281
+ }
282
+ for (var _i = 0, SUPPORTED_EXTENSIONS_2 = SUPPORTED_EXTENSIONS; _i < SUPPORTED_EXTENSIONS_2.length; _i++) {
283
+ var extension = SUPPORTED_EXTENSIONS_2[_i];
284
+ var candidate = path_1.default.join(basePath, "index".concat(extension));
285
+ var resolved = resolveFile(candidate);
286
+ if (resolved) {
287
+ return resolved;
288
+ }
289
+ }
290
+ return null;
291
+ };
292
+ // 将环路规范化为稳定 key,避免同一环路从不同入口重复报错。
293
+ var buildCycleKey = function (cycle) {
294
+ if (cycle.length <= 2) {
295
+ return cycle.join('->');
296
+ }
297
+ var unique = cycle.slice(0, -1).map(function (entry) { return path_1.default.normalize(entry); });
298
+ var best = unique.join('->');
299
+ for (var index = 1; index < unique.length; index += 1) {
300
+ var rotated = unique.slice(index).concat(unique.slice(0, index));
301
+ var candidate = rotated.join('->');
302
+ if (candidate < best) {
303
+ best = candidate;
304
+ }
305
+ }
306
+ return best;
307
+ };
308
+ // 输出尽量相对路径,便于直接定位文件。
309
+ var formatCycle = function (cycle, root) {
310
+ var formatted = cycle.map(function (entry) {
311
+ var relative = path_1.default.relative(root, entry);
312
+ if (!relative || relative.startsWith('..') || path_1.default.isAbsolute(relative)) {
313
+ return entry;
314
+ }
315
+ return relative;
316
+ });
317
+ return formatted.join(' -> ');
318
+ };
319
+ var buildLineIndex = function (source) {
320
+ var offsets = [0];
321
+ for (var index = 0; index < source.length; index += 1) {
322
+ if (source[index] === '\n') {
323
+ offsets.push(index + 1);
324
+ }
325
+ }
326
+ return { offsets: offsets };
327
+ };
328
+ var resolveLine = function (_a, position) {
329
+ var offsets = _a.offsets;
330
+ var low = 0;
331
+ var high = offsets.length - 1;
332
+ while (low <= high) {
333
+ var mid = Math.floor((low + high) / 2);
334
+ var current = offsets[mid];
335
+ if (current === position) {
336
+ return mid + 1;
337
+ }
338
+ if (current < position) {
339
+ low = mid + 1;
340
+ }
341
+ else {
342
+ high = mid - 1;
343
+ }
344
+ }
345
+ return high + 1;
346
+ };
347
+ var readUtf8Character = function (source, index, code) {
348
+ if (code <= 0x7f) {
349
+ return { bytes: 1, nextIndex: index + 1 };
350
+ }
351
+ if (code <= 0x7ff) {
352
+ return { bytes: 2, nextIndex: index + 1 };
353
+ }
354
+ if (code >= 0xd800 && code <= 0xdbff && index + 1 < source.length) {
355
+ var next = source.charCodeAt(index + 1);
356
+ if (next >= 0xdc00 && next <= 0xdfff) {
357
+ return { bytes: 4, nextIndex: index + 2 };
358
+ }
359
+ }
360
+ return { bytes: 3, nextIndex: index + 1 };
361
+ };
362
+ var bytePosToCharIndex = function (source, moduleStart, bytePos) {
363
+ var target = Math.max(bytePos - moduleStart, 0);
364
+ if (target === 0) {
365
+ return 0;
366
+ }
367
+ var index = 0;
368
+ var byteOffset = 0;
369
+ while (index < source.length) {
370
+ var code = source.charCodeAt(index);
371
+ var _a = readUtf8Character(source, index, code), bytes = _a.bytes, nextIndex = _a.nextIndex;
372
+ if (byteOffset + bytes > target) {
373
+ return index;
374
+ }
375
+ byteOffset += bytes;
376
+ index = nextIndex;
377
+ }
378
+ return source.length;
379
+ };
380
+ var findImportLine = function (source, value) {
381
+ var lines = source.split(/\r?\n/);
382
+ for (var index = 0; index < lines.length; index += 1) {
383
+ var line = lines[index];
384
+ if (line.includes('import') && line.includes(value)) {
385
+ return index + 1;
386
+ }
387
+ if (line.includes('require') && line.includes(value)) {
388
+ return index + 1;
389
+ }
390
+ }
391
+ return undefined;
392
+ };
393
+ var selectLineNumber = function (computed, fallback) {
394
+ if (fallback === undefined) {
395
+ return computed;
396
+ }
397
+ if (computed === undefined) {
398
+ return fallback;
399
+ }
400
+ if (computed < fallback) {
401
+ return fallback;
402
+ }
403
+ return computed;
404
+ };
@@ -3,4 +3,4 @@ export { RuleSeverity } from './types';
3
3
  export { defineRule, isRule, toRule } from './types';
4
4
  export { createRuleContext } from './runtime';
5
5
  export type { RuleContextOptions } from './runtime';
6
- export { builtinRules, noDeepRelativeImports, noSwallowedErrors } from './rules';
6
+ export { builtinRules, noDeepRelativeImports, noCircularDependencies, noSwallowedErrors } from './rules';
@@ -1,5 +1,6 @@
1
1
  import type { Rule } from '../types';
2
2
  import { noDeepRelativeImports } from './noDeepRelativeImports';
3
+ import { noCircularDependencies } from './noCircularDependencies';
3
4
  import { noSwallowedErrors } from './noSwallowedErrors';
4
5
  export declare const builtinRules: Rule[];
5
- export { noDeepRelativeImports, noSwallowedErrors };
6
+ export { noDeepRelativeImports, noCircularDependencies, noSwallowedErrors };
@@ -0,0 +1 @@
1
+ export declare const noCircularDependencies: import("../types").Rule;
@@ -42,6 +42,9 @@ export interface RuleMessages {
42
42
  maxDepth: number;
43
43
  }): string;
44
44
  swallowedError(): string;
45
+ circularDependency(params: {
46
+ chain: string;
47
+ }): string;
45
48
  }
46
49
  export interface RuleHelpers {
47
50
  readonly imports: ReadonlyArray<ImportReference>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-cr-rules",
3
- "version": "2.0.75",
3
+ "version": "2.0.78",
4
4
  "description": "Extensible static analysis rule set for the auto-cr automated code review toolkit",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/types/index.d.ts",
@@ -40,6 +40,7 @@
40
40
  "@swc/types": "^0.1.25"
41
41
  },
42
42
  "devDependencies": {
43
+ "@types/node": "^25.0.9",
43
44
  "tslib": "^2.8.1",
44
45
  "typescript": "^5.9.2"
45
46
  }