auto-cr-rules 2.0.37 → 2.0.40

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.
@@ -6,6 +6,7 @@ var MAX_DEPTH = 2;
6
6
  exports.noDeepRelativeImports = (0, types_1.defineRule)('no-deep-relative-imports', { tag: 'base', severity: types_1.RuleSeverity.Warning }, function (_a) {
7
7
  var _b, _c;
8
8
  var ast = _a.ast, helpers = _a.helpers, messages = _a.messages, language = _a.language, source = _a.source;
9
+ // Build a per-file line index so we can convert byte offsets emitted by SWC back to line numbers.
9
10
  var moduleStart = (_c = (_b = ast.span) === null || _b === void 0 ? void 0 : _b.start) !== null && _c !== void 0 ? _c : 0;
10
11
  var lineIndex = buildLineIndex(source);
11
12
  for (var _i = 0, _d = helpers.imports; _i < _d.length; _i++) {
@@ -29,6 +30,7 @@ exports.noDeepRelativeImports = (0, types_1.defineRule)('no-deep-relative-import
29
30
  ? resolveLine(lineIndex, bytePosToCharIndex(source, moduleStart, reference.span.start))
30
31
  : undefined;
31
32
  var fallbackLine = findImportLine(source, reference.value);
33
+ // Prefer the larger value so we never point at leading block comments when the byte offset is truncated.
32
34
  var line = selectLineNumber(computedLine, fallbackLine);
33
35
  helpers.reportViolation({
34
36
  description: description,
@@ -41,6 +43,7 @@ exports.noDeepRelativeImports = (0, types_1.defineRule)('no-deep-relative-import
41
43
  }
42
44
  });
43
45
  var buildLineIndex = function (source) {
46
+ // Track every newline so we can binary-search the surrounding line for any byte position.
44
47
  var offsets = [0];
45
48
  for (var index = 0; index < source.length; index += 1) {
46
49
  if (source[index] === '\n') {
@@ -105,6 +108,7 @@ var findImportLine = function (source, value) {
105
108
  var lines = source.split(/\r?\n/);
106
109
  for (var index = 0; index < lines.length; index += 1) {
107
110
  var line = lines[index];
111
+ // Look for the literal import string. This is slower than span math but provides a robust fallback.
108
112
  if (line.includes('import') && line.includes(value)) {
109
113
  return index + 1;
110
114
  }
@@ -112,6 +116,8 @@ var findImportLine = function (source, value) {
112
116
  return undefined;
113
117
  };
114
118
  var selectLineNumber = function (computed, fallback) {
119
+ // If one of the sources is missing, prefer the other. When both exist, use the larger line number so
120
+ // we avoid pointing at comment blocks above the actual statement.
115
121
  if (fallback === undefined) {
116
122
  return computed;
117
123
  }
@@ -67,6 +67,7 @@ var visitTryStatements = function (ast, callback) {
67
67
  exports.noSwallowedErrors = (0, types_1.defineRule)('no-swallowed-errors', { tag: 'base', severity: types_1.RuleSeverity.Warning }, function (_a) {
68
68
  var _b, _c;
69
69
  var ast = _a.ast, helpers = _a.helpers, messages = _a.messages, source = _a.source;
70
+ // Record the start of the module so we can normalise SWC's global byte offsets to file-local positions.
70
71
  var moduleStart = (_c = (_b = ast.span) === null || _b === void 0 ? void 0 : _b.start) !== null && _c !== void 0 ? _c : 0;
71
72
  var lineIndex = buildLineIndex(source);
72
73
  visitTryStatements(ast, function (statement) {
@@ -80,7 +81,11 @@ exports.noSwallowedErrors = (0, types_1.defineRule)('no-swallowed-errors', { tag
80
81
  var body = handler.body;
81
82
  var statements = body.stmts;
82
83
  var report = function () {
83
- var line = resolveLine(lineIndex, bytePosToCharIndex(source, moduleStart, body.span.start));
84
+ // Convert the body span to a line, then fall back to the literal catch line if the maths lands in comments.
85
+ var charIndex = bytePosToCharIndex(source, moduleStart, body.span.start);
86
+ var computedLine = resolveLine(lineIndex, charIndex);
87
+ var fallbackLine = findCatchLine(source, computedLine);
88
+ var line = selectLineNumber(computedLine, fallbackLine);
84
89
  helpers.reportViolation({
85
90
  description: messages.swallowedError(),
86
91
  line: line,
@@ -99,6 +104,7 @@ exports.noSwallowedErrors = (0, types_1.defineRule)('no-swallowed-errors', { tag
99
104
  });
100
105
  });
101
106
  var buildLineIndex = function (source) {
107
+ // Collect every newline. We share the helper with the import rule so behaviour stays consistent across detectors.
102
108
  var offsets = [0];
103
109
  for (var index = 0; index < source.length; index += 1) {
104
110
  if (source[index] === '\n') {
@@ -159,3 +165,28 @@ var bytePosToCharIndex = function (source, moduleStart, bytePos) {
159
165
  }
160
166
  return source.length;
161
167
  };
168
+ var findCatchLine = function (source, computedLine) {
169
+ var lines = source.split(/\r?\n/);
170
+ var startIndex = Math.max((computedLine !== null && computedLine !== void 0 ? computedLine : 1) - 1, 0);
171
+ var catchPattern = /\bcatch\b/;
172
+ // Walk forward from the computed line so we land on the actual catch clause even if decorators or comments exist.
173
+ for (var index = startIndex; index < lines.length; index += 1) {
174
+ if (catchPattern.test(lines[index])) {
175
+ return index + 1;
176
+ }
177
+ }
178
+ return undefined;
179
+ };
180
+ var selectLineNumber = function (computed, fallback) {
181
+ // Mirror the behaviour in the import rule: prefer the fallback when it is available and appears after the computed line.
182
+ if (fallback === undefined) {
183
+ return computed;
184
+ }
185
+ if (computed === undefined) {
186
+ return fallback;
187
+ }
188
+ if (computed < fallback) {
189
+ return fallback;
190
+ }
191
+ return computed;
192
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-cr-rules",
3
- "version": "2.0.37",
3
+ "version": "2.0.40",
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",
@@ -1,113 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.missingAsyncCleanup = void 0;
4
- var types_1 = require("../types");
5
- var isIdentifier = function (node, name) {
6
- return node.type === 'Identifier' && node.value === name;
7
- };
8
- var isMemberExpressionWithIdentifier = function (node, name) {
9
- if (node.type !== 'MemberExpression') {
10
- return false;
11
- }
12
- if (node.property.type === 'Identifier' && node.property.value === name) {
13
- return true;
14
- }
15
- return false;
16
- };
17
- var createMetrics = function () { return ({
18
- setInterval: {
19
- api: 'setInterval',
20
- cleanup: 'clearInterval',
21
- calls: [],
22
- cleanupCount: 0,
23
- codeSample: 'setInterval(() => {/* ... */}, delay)',
24
- },
25
- addEventListener: {
26
- api: 'addEventListener',
27
- cleanup: 'removeEventListener',
28
- calls: [],
29
- cleanupCount: 0,
30
- codeSample: 'target.addEventListener("type", handler)',
31
- },
32
- }); };
33
- function isTargetedCall(callee, name) {
34
- return isIdentifier(callee, name) || isMemberExpressionWithIdentifier(callee, name);
35
- }
36
- function visit(node, cb) {
37
- var _a;
38
- if (!node) {
39
- return;
40
- }
41
- if (Array.isArray(node)) {
42
- node.forEach(function (child) { return visit(child, cb); });
43
- return;
44
- }
45
- if (typeof node !== 'object') {
46
- return;
47
- }
48
- var candidate = node;
49
- if (candidate.type === 'CallExpression') {
50
- cb(candidate);
51
- var call = candidate;
52
- visit(call.callee, cb);
53
- if (call.arguments) {
54
- for (var _i = 0, _b = call.arguments; _i < _b.length; _i++) {
55
- var arg = _b[_i];
56
- visit((_a = arg.expression) !== null && _a !== void 0 ? _a : arg, cb);
57
- }
58
- }
59
- if (call.typeArguments) {
60
- visit(call.typeArguments, cb);
61
- }
62
- return;
63
- }
64
- for (var _c = 0, _d = Object.values(candidate); _c < _d.length; _c++) {
65
- var value = _d[_c];
66
- visit(value, cb);
67
- }
68
- }
69
- exports.missingAsyncCleanup = (0, types_1.defineRule)('require-cleanup-for-async-effects', { tag: 'base', severity: types_1.RuleSeverity.Warning }, function (_a) {
70
- var ast = _a.ast, helpers = _a.helpers, language = _a.language, messages = _a.messages;
71
- var metrics = createMetrics();
72
- visit(ast, function (call) {
73
- var callee = call.callee;
74
- if (isTargetedCall(callee, 'setInterval')) {
75
- metrics.setInterval.calls.push(call);
76
- return;
77
- }
78
- if (isTargetedCall(callee, 'clearInterval')) {
79
- metrics.setInterval.cleanupCount += 1;
80
- return;
81
- }
82
- if (isTargetedCall(callee, 'addEventListener')) {
83
- metrics.addEventListener.calls.push(call);
84
- return;
85
- }
86
- if (isTargetedCall(callee, 'removeEventListener')) {
87
- metrics.addEventListener.cleanupCount += 1;
88
- return;
89
- }
90
- });
91
- var suggestionsByLanguage = language === 'zh'
92
- ? [
93
- { text: '在 useEffect 的返回函数或组件卸载时调用对应的清理函数。' },
94
- { text: '确保自定义 Hook 在调用组件卸载时执行 clearInterval/removeEventListener。' },
95
- ]
96
- : [
97
- { text: 'Return a cleanup function from useEffect to clear timers or listeners.' },
98
- { text: 'Ensure custom hooks expose a teardown that calls clearInterval/removeEventListener.' },
99
- ];
100
- Object.values(metrics).forEach(function (metric) {
101
- if (metric.calls.length === 0 || metric.cleanupCount > 0) {
102
- return;
103
- }
104
- var description = messages.missingAsyncCleanup({ api: metric.api, cleanup: metric.cleanup });
105
- var firstCall = metric.calls[0];
106
- helpers.reportViolation({
107
- description: description,
108
- code: metric.codeSample,
109
- suggestions: suggestionsByLanguage,
110
- span: firstCall.span,
111
- }, firstCall.span);
112
- });
113
- });
@@ -1,101 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.noUnsafeDangerouslySetInnerHTML = void 0;
4
- var types_1 = require("../types");
5
- var ATTRIBUTE_NAME = 'dangerouslySetInnerHTML';
6
- var HTML_KEY = '__html';
7
- var isIdentifierAttribute = function (attribute, name) {
8
- return attribute.name.type === 'Identifier' && attribute.name.value === name;
9
- };
10
- var isTrustedLiteral = function (expression) {
11
- if (expression.type === 'StringLiteral') {
12
- return true;
13
- }
14
- if (expression.type === 'TemplateLiteral') {
15
- var template = expression;
16
- return template.expressions.length === 0;
17
- }
18
- return false;
19
- };
20
- var extractHtmlExpression = function (attribute) {
21
- if (!attribute.value) {
22
- return null;
23
- }
24
- if (attribute.value.type === 'StringLiteral') {
25
- return attribute.value;
26
- }
27
- if (attribute.value.type !== 'JSXExpressionContainer') {
28
- return null;
29
- }
30
- var container = attribute.value;
31
- var expression = container.expression;
32
- if (!expression || expression.type === 'JSXEmptyExpression') {
33
- return null;
34
- }
35
- if (expression.type === 'ObjectExpression') {
36
- for (var _i = 0, _a = expression.properties; _i < _a.length; _i++) {
37
- var property = _a[_i];
38
- if (property.type !== 'KeyValueProperty') {
39
- continue;
40
- }
41
- var key = property.key;
42
- var keyName = key.type === 'Identifier'
43
- ? key.value
44
- : key.type === 'StringLiteral'
45
- ? key.value
46
- : null;
47
- if (keyName === HTML_KEY) {
48
- return property.value;
49
- }
50
- }
51
- return expression;
52
- }
53
- return expression;
54
- };
55
- var visit = function (node, callback) {
56
- if (!node) {
57
- return;
58
- }
59
- if (Array.isArray(node)) {
60
- node.forEach(function (child) { return visit(child, callback); });
61
- return;
62
- }
63
- if (typeof node !== 'object') {
64
- return;
65
- }
66
- var candidate = node;
67
- if (candidate.type === 'JSXAttribute') {
68
- callback(candidate);
69
- }
70
- for (var _i = 0, _a = Object.values(candidate); _i < _a.length; _i++) {
71
- var value = _a[_i];
72
- visit(value, callback);
73
- }
74
- };
75
- exports.noUnsafeDangerouslySetInnerHTML = (0, types_1.defineRule)('no-unsafe-dangerously-set-inner-html', { tag: 'security', severity: types_1.RuleSeverity.Error }, function (_a) {
76
- var ast = _a.ast, helpers = _a.helpers, language = _a.language, messages = _a.messages;
77
- visit(ast, function (attribute) {
78
- if (!isIdentifierAttribute(attribute, ATTRIBUTE_NAME)) {
79
- return;
80
- }
81
- var expression = extractHtmlExpression(attribute);
82
- if (expression && isTrustedLiteral(expression)) {
83
- return;
84
- }
85
- var suggestions = language === 'zh'
86
- ? [
87
- { text: '优先使用经过消毒的 HTML(例如 DOMPurify.sanitize)。' },
88
- { text: '避免直接渲染来自外部或用户输入的字符串。' },
89
- ]
90
- : [
91
- { text: 'Sanitize the HTML string before passing it in (e.g. DOMPurify.sanitize).' },
92
- { text: 'Avoid rendering user-generated strings with dangerouslySetInnerHTML.' },
93
- ];
94
- helpers.reportViolation({
95
- description: messages.unsafeDangerouslySetInnerHTML(),
96
- code: ATTRIBUTE_NAME,
97
- suggestions: suggestions,
98
- span: attribute.span,
99
- });
100
- });
101
- });
@@ -1 +0,0 @@
1
- export declare const missingAsyncCleanup: import("../types").Rule;
@@ -1 +0,0 @@
1
- export declare const noUnsafeDangerouslySetInnerHTML: import("../types").Rule;