auto-cr-rules 2.0.40 → 2.0.41
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/rules/missingAsyncCleanup.js +113 -0
- package/dist/rules/noSwallowedErrors.js +64 -98
- package/dist/rules/noUnsafeDangerouslySetInnerHTML.js +101 -0
- package/dist/types/rules/missingAsyncCleanup.d.ts +1 -0
- package/dist/types/rules/noUnsafeDangerouslySetInnerHTML.d.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
});
|
|
@@ -2,109 +2,38 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.noSwallowedErrors = void 0;
|
|
4
4
|
var types_1 = require("../types");
|
|
5
|
-
var LOG_METHODS = new Set(['error', 'warn', 'info', 'log', 'fatal']);
|
|
6
|
-
var isLoggingCall = function (expression) {
|
|
7
|
-
var callee = expression.callee;
|
|
8
|
-
if (callee.type === 'Identifier') {
|
|
9
|
-
var name = callee.value.toLowerCase();
|
|
10
|
-
return name.includes('log') || name.includes('error') || name.includes('report');
|
|
11
|
-
}
|
|
12
|
-
if (callee.type === 'MemberExpression') {
|
|
13
|
-
var member = callee;
|
|
14
|
-
var property = member.property;
|
|
15
|
-
if (property.type === 'Identifier' && LOG_METHODS.has(property.value)) {
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return false;
|
|
20
|
-
};
|
|
21
|
-
var hasLoggingCall = function (statements) {
|
|
22
|
-
for (var _i = 0, statements_1 = statements; _i < statements_1.length; _i++) {
|
|
23
|
-
var statement = statements_1[_i];
|
|
24
|
-
if (statement.type === 'ExpressionStatement' && statement.expression.type === 'CallExpression') {
|
|
25
|
-
if (isLoggingCall(statement.expression)) {
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return false;
|
|
31
|
-
};
|
|
32
|
-
var containsThrowStatement = function (node) {
|
|
33
|
-
var queue = [node];
|
|
34
|
-
while (queue.length > 0) {
|
|
35
|
-
var current = queue.pop();
|
|
36
|
-
if (!current || typeof current !== 'object') {
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
var candidate = current;
|
|
40
|
-
if (candidate.type === 'ThrowStatement') {
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
for (var _i = 0, _a = Object.values(candidate); _i < _a.length; _i++) {
|
|
44
|
-
var value = _a[_i];
|
|
45
|
-
queue.push(value);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return false;
|
|
49
|
-
};
|
|
50
|
-
var visitTryStatements = function (ast, callback) {
|
|
51
|
-
var queue = [ast];
|
|
52
|
-
while (queue.length > 0) {
|
|
53
|
-
var current = queue.pop();
|
|
54
|
-
if (!current || typeof current !== 'object') {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
var candidate = current;
|
|
58
|
-
if (candidate.type === 'TryStatement') {
|
|
59
|
-
callback(candidate);
|
|
60
|
-
}
|
|
61
|
-
for (var _i = 0, _a = Object.values(candidate); _i < _a.length; _i++) {
|
|
62
|
-
var value = _a[_i];
|
|
63
|
-
queue.push(value);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
5
|
exports.noSwallowedErrors = (0, types_1.defineRule)('no-swallowed-errors', { tag: 'base', severity: types_1.RuleSeverity.Warning }, function (_a) {
|
|
68
6
|
var _b, _c;
|
|
69
7
|
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.
|
|
71
8
|
var moduleStart = (_c = (_b = ast.span) === null || _b === void 0 ? void 0 : _b.start) !== null && _c !== void 0 ? _c : 0;
|
|
72
9
|
var lineIndex = buildLineIndex(source);
|
|
73
|
-
visitTryStatements(ast, function (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
var
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
var body = handler.body;
|
|
82
|
-
var statements = body.stmts;
|
|
83
|
-
var report = function () {
|
|
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);
|
|
89
|
-
helpers.reportViolation({
|
|
90
|
-
description: messages.swallowedError(),
|
|
91
|
-
line: line,
|
|
92
|
-
span: body.span,
|
|
93
|
-
}, body.span);
|
|
94
|
-
};
|
|
95
|
-
if (statements.length === 0) {
|
|
96
|
-
report();
|
|
10
|
+
visitTryStatements(ast, function (tryStatement) {
|
|
11
|
+
var _a, _b, _c, _d, _e;
|
|
12
|
+
var catchBlock = (_b = (_a = tryStatement.handler) === null || _a === void 0 ? void 0 : _a.body) !== null && _b !== void 0 ? _b : null;
|
|
13
|
+
var finallyBlock = (_c = tryStatement.finalizer) !== null && _c !== void 0 ? _c : null;
|
|
14
|
+
var catchHasExecutable = catchBlock ? hasExecutableStatements(catchBlock.stmts) : false;
|
|
15
|
+
var finallyHasExecutable = finallyBlock ? hasExecutableStatements(finallyBlock.stmts) : false;
|
|
16
|
+
if (catchHasExecutable || finallyHasExecutable) {
|
|
97
17
|
return;
|
|
98
18
|
}
|
|
99
|
-
var
|
|
100
|
-
var
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
19
|
+
var reportSpan = (_e = (_d = catchBlock === null || catchBlock === void 0 ? void 0 : catchBlock.span) !== null && _d !== void 0 ? _d : finallyBlock === null || finallyBlock === void 0 ? void 0 : finallyBlock.span) !== null && _e !== void 0 ? _e : tryStatement.span;
|
|
20
|
+
var charIndex = bytePosToCharIndex(source, moduleStart, reportSpan.start);
|
|
21
|
+
var computedLine = resolveLine(lineIndex, charIndex);
|
|
22
|
+
var fallbackLine = determineFallbackLine({
|
|
23
|
+
source: source,
|
|
24
|
+
computedLine: computedLine,
|
|
25
|
+
hasCatch: Boolean(catchBlock),
|
|
26
|
+
hasFinally: Boolean(finallyBlock),
|
|
27
|
+
});
|
|
28
|
+
var line = selectLineNumber(computedLine, fallbackLine);
|
|
29
|
+
helpers.reportViolation({
|
|
30
|
+
description: messages.swallowedError(),
|
|
31
|
+
line: line,
|
|
32
|
+
span: reportSpan,
|
|
33
|
+
}, reportSpan);
|
|
104
34
|
});
|
|
105
35
|
});
|
|
106
36
|
var buildLineIndex = function (source) {
|
|
107
|
-
// Collect every newline. We share the helper with the import rule so behaviour stays consistent across detectors.
|
|
108
37
|
var offsets = [0];
|
|
109
38
|
for (var index = 0; index < source.length; index += 1) {
|
|
110
39
|
if (source[index] === '\n') {
|
|
@@ -165,20 +94,27 @@ var bytePosToCharIndex = function (source, moduleStart, bytePos) {
|
|
|
165
94
|
}
|
|
166
95
|
return source.length;
|
|
167
96
|
};
|
|
168
|
-
var
|
|
97
|
+
var determineFallbackLine = function (_a) {
|
|
98
|
+
var source = _a.source, computedLine = _a.computedLine, hasCatch = _a.hasCatch, hasFinally = _a.hasFinally;
|
|
99
|
+
if (hasCatch) {
|
|
100
|
+
return findKeywordLine(source, computedLine, /\bcatch\b/);
|
|
101
|
+
}
|
|
102
|
+
if (hasFinally) {
|
|
103
|
+
return findKeywordLine(source, computedLine, /\bfinally\b/);
|
|
104
|
+
}
|
|
105
|
+
return findKeywordLine(source, computedLine, /\btry\b/);
|
|
106
|
+
};
|
|
107
|
+
var findKeywordLine = function (source, computedLine, pattern) {
|
|
169
108
|
var lines = source.split(/\r?\n/);
|
|
170
109
|
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
110
|
for (var index = startIndex; index < lines.length; index += 1) {
|
|
174
|
-
if (
|
|
111
|
+
if (pattern.test(lines[index])) {
|
|
175
112
|
return index + 1;
|
|
176
113
|
}
|
|
177
114
|
}
|
|
178
115
|
return undefined;
|
|
179
116
|
};
|
|
180
117
|
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
118
|
if (fallback === undefined) {
|
|
183
119
|
return computed;
|
|
184
120
|
}
|
|
@@ -190,3 +126,33 @@ var selectLineNumber = function (computed, fallback) {
|
|
|
190
126
|
}
|
|
191
127
|
return computed;
|
|
192
128
|
};
|
|
129
|
+
var visitTryStatements = function (ast, callback) {
|
|
130
|
+
var queue = [ast];
|
|
131
|
+
while (queue.length > 0) {
|
|
132
|
+
var current = queue.pop();
|
|
133
|
+
if (!current || typeof current !== 'object') {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
var candidate = current;
|
|
137
|
+
if (candidate.type === 'TryStatement') {
|
|
138
|
+
callback(candidate);
|
|
139
|
+
}
|
|
140
|
+
for (var _i = 0, _a = Object.values(candidate); _i < _a.length; _i++) {
|
|
141
|
+
var value = _a[_i];
|
|
142
|
+
queue.push(value);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
var hasExecutableStatements = function (statements) {
|
|
147
|
+
return statements.some(isExecutableStatement);
|
|
148
|
+
};
|
|
149
|
+
var isExecutableStatement = function (statement) {
|
|
150
|
+
switch (statement.type) {
|
|
151
|
+
case 'EmptyStatement':
|
|
152
|
+
return false;
|
|
153
|
+
case 'BlockStatement':
|
|
154
|
+
return hasExecutableStatements(statement.stmts);
|
|
155
|
+
default:
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const missingAsyncCleanup: import("../types").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noUnsafeDangerouslySetInnerHTML: import("../types").Rule;
|
package/package.json
CHANGED