auto-cr-rules 2.0.63 → 2.0.66
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/README.md +42 -0
- package/README.zh-CN.md +52 -0
- package/package.json +1 -1
- package/dist/rules/missingAsyncCleanup.js +0 -113
- package/dist/rules/noUnsafeDangerouslySetInnerHTML.js +0 -101
- package/dist/types/rules/missingAsyncCleanup.d.ts +0 -1
- package/dist/types/rules/noUnsafeDangerouslySetInnerHTML.d.ts +0 -1
package/README.md
CHANGED
|
@@ -43,6 +43,9 @@ Common flags:
|
|
|
43
43
|
- `--language <zh|en>`: Switch CLI output language (defaults to auto-detection).
|
|
44
44
|
- `--rule-dir <directory>`: Load additional custom rules from a directory or package.
|
|
45
45
|
- `--output <text|json>`: Choose between human-friendly text logs or structured JSON results (defaults to `text`).
|
|
46
|
+
- `--config <path>`: Point to a `.autocrrc.json` or `.autocrrc.js` file to enable/disable rules.
|
|
47
|
+
- `--ignore-path <path>`: Point to a `.autocrignore.json` or `.autocrignore.js` file to exclude files/directories from scanning.
|
|
48
|
+
- `--tsconfig <path>`: Use a custom `tsconfig.json` (defaults to `<cwd>/tsconfig.json`).
|
|
46
49
|
- `--help`: Display the full command reference.
|
|
47
50
|
|
|
48
51
|
Sample output:
|
|
@@ -108,6 +111,45 @@ npx auto-cr-cmd --output json -- ./src | jq
|
|
|
108
111
|
}
|
|
109
112
|
```
|
|
110
113
|
|
|
114
|
+
## Configuration (.autocrrc)
|
|
115
|
+
|
|
116
|
+
- Place `.autocrrc.json` or `.autocrrc.js` in your repo root (search order as listed). Use `--config <path>` to point elsewhere.
|
|
117
|
+
- `rules` accepts `off | warning | error | optimizing | true/false | 0/1/2`; unspecified rules keep their default severity.
|
|
118
|
+
|
|
119
|
+
```jsonc
|
|
120
|
+
// .autocrrc.json
|
|
121
|
+
{
|
|
122
|
+
"rules": {
|
|
123
|
+
"no-deep-relative-imports": "error",
|
|
124
|
+
"no-swallowed-errors": "off"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Ignore paths (.autocrignore)
|
|
130
|
+
|
|
131
|
+
- Place `.autocrignore.json` or `.autocrignore.js` in repo root (search order as listed), or pass `--ignore-path <file>`.
|
|
132
|
+
- Supports glob patterns (picomatch) via JSON/JS arrays (`{ ignore: [...] }`).
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
// .autocrignore.js
|
|
136
|
+
module.exports = {
|
|
137
|
+
ignore: ['node_modules', 'dist/**', '**/*.test.ts', 'public/**']
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
// .autocrignore.json
|
|
143
|
+
{
|
|
144
|
+
"ignore": [
|
|
145
|
+
"node_modules",
|
|
146
|
+
"dist/**",
|
|
147
|
+
"**/*.test.ts",
|
|
148
|
+
"public/**"
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
111
153
|
## Writing Custom Rules
|
|
112
154
|
|
|
113
155
|
The CLI consumes rules from the `auto-cr-rules` package by default, and you can extend it with your own logic.
|
package/README.zh-CN.md
CHANGED
|
@@ -43,6 +43,9 @@ npx auto-cr-cmd --language zh [需要扫描的代码目录]
|
|
|
43
43
|
- `--language <zh|en>`:切换 CLI 输出语言(默认为自动检测)。
|
|
44
44
|
- `--rule-dir <directory>`:加载额外的自定义规则目录或包。
|
|
45
45
|
- `--output <text|json>`:选择输出格式,`text` 为友好的终端日志,`json` 用于集成脚本(默认为 `text`)。
|
|
46
|
+
- `--config <path>`:指定 `.autocrrc.json` 或 `.autocrrc.js` 配置文件路径,用于开启/关闭规则。
|
|
47
|
+
- `--ignore-path <path>`:指定 `.autocrignore.json` 或 `.autocrignore.js` 忽略文件路径,用于排除扫描。
|
|
48
|
+
- `--tsconfig <path>`:指定自定义 `tsconfig.json` 路径(默认读取 `<cwd>/tsconfig.json`)。
|
|
46
49
|
- `--help`:查看完整命令说明。
|
|
47
50
|
|
|
48
51
|
示例输出:
|
|
@@ -108,6 +111,55 @@ npx auto-cr-cmd --output json -- ./src | jq
|
|
|
108
111
|
}
|
|
109
112
|
```
|
|
110
113
|
|
|
114
|
+
## 配置(.autocrrc)
|
|
115
|
+
|
|
116
|
+
- 在仓库根目录放置 `.autocrrc.json` 或 `.autocrrc.js`(按此顺序查找);如需放在其他位置,可通过 `--config <path>` 指定。
|
|
117
|
+
- `rules` 支持的值:`off | warning | error | optimizing | true/false | 0/1/2`,未写明的规则沿用默认严重级别。
|
|
118
|
+
|
|
119
|
+
```jsonc
|
|
120
|
+
// .autocrrc.json
|
|
121
|
+
{
|
|
122
|
+
"rules": {
|
|
123
|
+
"no-deep-relative-imports": "error",
|
|
124
|
+
"no-swallowed-errors": "off"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 忽略文件(.autocrignore)
|
|
130
|
+
|
|
131
|
+
- 在仓库根目录放置 `.autocrignore.json` 或 `.autocrignore.js`(按此顺序查找),或通过 `--ignore-path <file>` 指定自定义路径。
|
|
132
|
+
- 仅支持 JSON/JS 写法,基于 picomatch 的 glob 模式,数组键为 `ignore`。
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
// .autocrignore.js
|
|
136
|
+
module.exports = {
|
|
137
|
+
ignore: ['node_modules', 'dist/**', '**/*.test.ts', 'public/**']
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
// .autocrignore.json
|
|
143
|
+
{
|
|
144
|
+
"ignore": [
|
|
145
|
+
"node_modules",
|
|
146
|
+
"dist/**",
|
|
147
|
+
"**/*.test.ts",
|
|
148
|
+
"public/**"
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
// .autocrrc.js
|
|
155
|
+
module.exports = {
|
|
156
|
+
rules: {
|
|
157
|
+
'no-swallowed-errors': 'warning', // 覆盖严重级别
|
|
158
|
+
'no-deep-relative-imports': true // 保持规则默认严重级别
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
111
163
|
## 编写自定义规则
|
|
112
164
|
|
|
113
165
|
CLI 默认使用 `auto-cr-rules` 包提供的规则,你也可以扩展自己的逻辑。
|
package/package.json
CHANGED
|
@@ -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;
|