@yss123/cage-lint 0.0.0-stage → 0.1.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/README.md +18 -2
- package/dependency-cruiser.cjs +41 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/no-manual-routes.d.ts +3 -0
- package/dist/rules/no-manual-routes.js +28 -0
- package/dist/rules/no-manual-routes.js.map +1 -0
- package/dist/rules/no-secret-literal.d.ts +3 -0
- package/dist/rules/no-secret-literal.js +46 -0
- package/dist/rules/no-secret-literal.js.map +1 -0
- package/dist/rules/no-sql-concat.d.ts +3 -0
- package/dist/rules/no-sql-concat.js +50 -0
- package/dist/rules/no-sql-concat.js.map +1 -0
- package/dist/rules/unsafe-needs-reason.d.ts +3 -0
- package/dist/rules/unsafe-needs-reason.js +27 -0
- package/dist/rules/unsafe-needs-reason.js.map +1 -0
- package/package.json +47 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 shunshunyue and Cage contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @yss123/cage-lint
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
dependency-cruiser 规则与 ESLint 插件
|
|
4
|
+
|
|
5
|
+
Cage 是通过 TypeScript 声明业务、由框架统一执行权限和数据规则的中后台框架。此包是框架的一部分。
|
|
6
|
+
|
|
7
|
+
## 使用
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
pnpm add -D @yss123/cage-lint
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
首次使用建议通过脚手架创建完整项目,所需依赖由模板统一配置。需要 Node.js 24 或更新版本。
|
|
14
|
+
|
|
15
|
+
- [完整使用说明](https://github.com/shunshunyue/cage/blob/main/docs/使用说明.md)
|
|
16
|
+
- [框架源码与范例](https://github.com/shunshunyue/cage)
|
|
17
|
+
- [问题反馈](https://github.com/shunshunyue/cage/issues)
|
|
18
|
+
|
|
19
|
+
许可证:[MIT](https://github.com/shunshunyue/cage/blob/main/LICENSE)。
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// 依赖规则(框架提供):管「谁不能导入谁」。项目里的 .dependency-cruiser.cjs 只需要一行:
|
|
2
|
+
// module.exports = require('@yss123/cage-lint/dependency-cruiser.cjs');
|
|
3
|
+
module.exports = {
|
|
4
|
+
forbidden: [
|
|
5
|
+
{
|
|
6
|
+
name: 'domain-no-io',
|
|
7
|
+
severity: 'error',
|
|
8
|
+
comment: '[CAGE-DEP-001] 声明和 handler 不能直接做 IO',
|
|
9
|
+
from: { path: '^packages/domain', pathNot: '\\.(test|spec)\\.' },
|
|
10
|
+
to: {
|
|
11
|
+
path: '^(node:)?(fs|net|http|https|http2|child_process|dgram|tls|worker_threads|cluster)$|node_modules/(pg|kysely|axios|undici|node-fetch|got)/|node_modules/@yss123/cage-http/',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'adapters-write-via-commands',
|
|
16
|
+
severity: 'error',
|
|
17
|
+
comment: '[CAGE-DEP-002] 适配器必须通过命令网关写数据',
|
|
18
|
+
from: { path: '^packages/adapters' },
|
|
19
|
+
to: { path: 'node_modules/(pg|kysely)/' },
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'web-only-contracts',
|
|
23
|
+
severity: 'error',
|
|
24
|
+
comment: '[CAGE-DEP-003] 前端只能依赖 @app/contracts',
|
|
25
|
+
from: { path: '^apps/web' },
|
|
26
|
+
to: { path: '^packages/domain|node_modules/@yss123/cage-(kernel|http|admin|connectors|approval|testkit)/' },
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'no-generated-imports-from-domain',
|
|
30
|
+
severity: 'error',
|
|
31
|
+
comment: '[CAGE-DEP-005] domain 不能依赖生成物',
|
|
32
|
+
from: { path: '^packages/domain' },
|
|
33
|
+
to: { path: '^(generated|packages/contracts)/' },
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
options: {
|
|
37
|
+
doNotFollow: { path: 'node_modules' },
|
|
38
|
+
tsPreCompilationDeps: true,
|
|
39
|
+
exclude: { path: '(^|/)(dist|generated/tests|node_modules)/' },
|
|
40
|
+
},
|
|
41
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ESLint, Linter } from 'eslint';
|
|
2
|
+
export declare const rules: {
|
|
3
|
+
'unsafe-needs-reason': import("eslint").Rule.RuleModule;
|
|
4
|
+
'no-sql-concat': import("eslint").Rule.RuleModule;
|
|
5
|
+
'no-secret-literal': import("eslint").Rule.RuleModule;
|
|
6
|
+
'no-manual-routes': import("eslint").Rule.RuleModule;
|
|
7
|
+
};
|
|
8
|
+
declare const plugin: ESLint.Plugin & {
|
|
9
|
+
configs: Record<string, Linter.Config[]>;
|
|
10
|
+
};
|
|
11
|
+
export default plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @yss123/cage-lint:ESLint 插件与推荐配置。项目里的 eslint.config.js:
|
|
2
|
+
// import cage from '@yss123/cage-lint';
|
|
3
|
+
// export default [...cage.configs.recommended];
|
|
4
|
+
import tsParser from '@typescript-eslint/parser';
|
|
5
|
+
import noManualRoutes from './rules/no-manual-routes.js';
|
|
6
|
+
import noSecretLiteral from './rules/no-secret-literal.js';
|
|
7
|
+
import noSqlConcat from './rules/no-sql-concat.js';
|
|
8
|
+
import unsafeNeedsReason from './rules/unsafe-needs-reason.js';
|
|
9
|
+
export const rules = {
|
|
10
|
+
'unsafe-needs-reason': unsafeNeedsReason,
|
|
11
|
+
'no-sql-concat': noSqlConcat,
|
|
12
|
+
'no-secret-literal': noSecretLiteral,
|
|
13
|
+
'no-manual-routes': noManualRoutes,
|
|
14
|
+
};
|
|
15
|
+
const plugin = {
|
|
16
|
+
meta: { name: '@yss123/cage-lint', version: '0.1.0' },
|
|
17
|
+
rules: rules,
|
|
18
|
+
configs: {},
|
|
19
|
+
};
|
|
20
|
+
plugin.configs.recommended = [
|
|
21
|
+
{ ignores: ['**/node_modules/**', '**/dist/**', 'generated/**', 'packages/contracts/**', '.cage/**'] },
|
|
22
|
+
{
|
|
23
|
+
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.mjs'],
|
|
24
|
+
languageOptions: {
|
|
25
|
+
parser: tsParser,
|
|
26
|
+
parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true } },
|
|
27
|
+
},
|
|
28
|
+
plugins: { cage: plugin },
|
|
29
|
+
rules: {
|
|
30
|
+
'cage/unsafe-needs-reason': 'error',
|
|
31
|
+
'cage/no-sql-concat': 'error',
|
|
32
|
+
'cage/no-secret-literal': 'error',
|
|
33
|
+
'cage/no-manual-routes': 'error',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
export default plugin;
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAC1D,0CAA0C;AAC1C,kDAAkD;AAClD,OAAO,QAAQ,MAAM,2BAA2B,CAAC;AAEjD,OAAO,cAAc,MAAM,6BAA6B,CAAC;AACzD,OAAO,eAAe,MAAM,8BAA8B,CAAC;AAC3D,OAAO,WAAW,MAAM,0BAA0B,CAAC;AACnD,OAAO,iBAAiB,MAAM,gCAAgC,CAAC;AAE/D,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,qBAAqB,EAAE,iBAAiB;IACxC,eAAe,EAAE,WAAW;IAC5B,mBAAmB,EAAE,eAAe;IACpC,kBAAkB,EAAE,cAAc;CACnC,CAAC;AAEF,MAAM,MAAM,GAAiE;IAC3E,IAAI,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,OAAO,EAAE;IACrD,KAAK,EAAE,KAAY;IACnB,OAAO,EAAE,EAAE;CACZ,CAAC;AAEF,MAAM,CAAC,OAAO,CAAC,WAAW,GAAG;IAC3B,EAAE,OAAO,EAAE,CAAC,oBAAoB,EAAE,YAAY,EAAE,cAAc,EAAE,uBAAuB,EAAE,UAAU,CAAC,EAAE;IACtG;QACE,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,CAAC;QACrD,eAAe,EAAE;YACf,MAAM,EAAE,QAAe;YACvB,aAAa,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;SAC5F;QACD,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;QACzB,KAAK,EAAE;YACL,0BAA0B,EAAE,OAAO;YACnC,oBAAoB,EAAE,OAAO;YAC7B,wBAAwB,EAAE,OAAO;YACjC,uBAAuB,EAAE,OAAO;SACjC;KACF;CACF,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'route', 'all', 'head', 'options']);
|
|
2
|
+
const RECEIVERS = /^(app|server|fastify|router|api|instance)$/i;
|
|
3
|
+
const rule = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'problem',
|
|
6
|
+
docs: { description: '禁止在业务项目里手写 HTTP 路由' },
|
|
7
|
+
messages: { route: '[CAGE-ROUTE-001] 不要手写路由 {{call}}:在 domain 里声明命令或查询,路由由框架生成' },
|
|
8
|
+
schema: [],
|
|
9
|
+
},
|
|
10
|
+
create(context) {
|
|
11
|
+
return {
|
|
12
|
+
CallExpression(node) {
|
|
13
|
+
const c = node.callee;
|
|
14
|
+
if (c.type !== 'MemberExpression' || c.property?.type !== 'Identifier' || !METHODS.has(c.property.name))
|
|
15
|
+
return;
|
|
16
|
+
const recv = c.object?.type === 'Identifier' ? c.object.name : c.object?.type === 'MemberExpression' ? c.object.property?.name : '';
|
|
17
|
+
const first = node.arguments[0];
|
|
18
|
+
const isPath = (first?.type === 'Literal' && typeof first.value === 'string' && first.value.startsWith('/')) ||
|
|
19
|
+
(first?.type === 'TemplateLiteral' && String(first.quasis[0]?.value?.raw ?? '').startsWith('/')) ||
|
|
20
|
+
(first?.type === 'ObjectExpression' && first.properties.some((p) => (p.key?.name ?? p.key?.value) === 'url'));
|
|
21
|
+
if (RECEIVERS.test(recv ?? '') && isPath)
|
|
22
|
+
context.report({ node, messageId: 'route', data: { call: `${recv}.${c.property.name}()` } });
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
export default rule;
|
|
28
|
+
//# sourceMappingURL=no-manual-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-manual-routes.js","sourceRoot":"","sources":["../../src/rules/no-manual-routes.ts"],"names":[],"mappings":"AAGA,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;AACtG,MAAM,SAAS,GAAG,6CAA6C,CAAC;AAEhE,MAAM,IAAI,GAAoB;IAC5B,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,EAAE,WAAW,EAAE,oBAAoB,EAAE;QAC3C,QAAQ,EAAE,EAAE,KAAK,EAAE,4DAA4D,EAAE;QACjF,MAAM,EAAE,EAAE;KACX;IACD,MAAM,CAAC,OAAO;QACZ,OAAO;YACL,cAAc,CAAC,IAAS;gBACtB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;gBACtB,IAAI,CAAC,CAAC,IAAI,KAAK,kBAAkB,IAAI,CAAC,CAAC,QAAQ,EAAE,IAAI,KAAK,YAAY,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;oBAAE,OAAO;gBAChH,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,KAAK,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;gBACpI,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;gBAChC,MAAM,MAAM,GACV,CAAC,KAAK,EAAE,IAAI,KAAK,SAAS,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;oBAC7F,CAAC,KAAK,EAAE,IAAI,KAAK,iBAAiB,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;oBAChG,CAAC,KAAK,EAAE,IAAI,KAAK,kBAAkB,IAAI,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC;gBACrH,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,IAAI,MAAM;oBAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,GAAG,IAAI,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC;YACzI,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC;AACF,eAAe,IAAI,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const PATTERNS = [
|
|
2
|
+
[/AKIA[0-9A-Z]{16}/, 'AWS 访问密钥'],
|
|
3
|
+
[/-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/, '私钥'],
|
|
4
|
+
[/\b(sk|rk|pk)_(live|test)_[0-9a-zA-Z]{16,}/, '支付平台密钥'],
|
|
5
|
+
[/\bsk-[A-Za-z0-9_-]{20,}/, 'API 密钥'],
|
|
6
|
+
[/\bgh[pousr]_[A-Za-z0-9]{30,}/, 'GitHub 令牌'],
|
|
7
|
+
[/\bxox[baprs]-[A-Za-z0-9-]{10,}/, 'Slack 令牌'],
|
|
8
|
+
[/eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/, 'JWT'],
|
|
9
|
+
];
|
|
10
|
+
const NAME_RE = /(password|passwd|secret|token|api[_-]?key|access[_-]?key|private[_-]?key)$/i;
|
|
11
|
+
const rule = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'problem',
|
|
14
|
+
docs: { description: '禁止密钥字面量' },
|
|
15
|
+
messages: { secret: '[CAGE-SECRET-002] 疑似{{what}}写在了代码里:密钥只写名字,值由密钥管理服务注入' },
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
const check = (node, value, name) => {
|
|
20
|
+
for (const [re, what] of PATTERNS) {
|
|
21
|
+
if (re.test(value)) {
|
|
22
|
+
context.report({ node, messageId: 'secret', data: { what } });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// 名字像密钥、值又不像「密钥名」(全大写下划线)的长字符串
|
|
27
|
+
if (name && NAME_RE.test(name) && value.length >= 12 && !/^[A-Z][A-Z0-9_]+$/.test(value) && !/\s/.test(value)) {
|
|
28
|
+
context.report({ node, messageId: 'secret', data: { what: '密钥' } });
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
Literal(node) {
|
|
33
|
+
if (typeof node.value !== 'string')
|
|
34
|
+
return;
|
|
35
|
+
const p = node.parent;
|
|
36
|
+
const name = p?.type === 'VariableDeclarator' ? p.id?.name
|
|
37
|
+
: p?.type === 'Property' ? (p.key?.name ?? p.key?.value)
|
|
38
|
+
: p?.type === 'AssignmentExpression' ? p.left?.property?.name
|
|
39
|
+
: undefined;
|
|
40
|
+
check(node, node.value, name);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
export default rule;
|
|
46
|
+
//# sourceMappingURL=no-secret-literal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-secret-literal.js","sourceRoot":"","sources":["../../src/rules/no-secret-literal.ts"],"names":[],"mappings":"AAGA,MAAM,QAAQ,GAAuB;IACnC,CAAC,kBAAkB,EAAE,UAAU,CAAC;IAChC,CAAC,iDAAiD,EAAE,IAAI,CAAC;IACzD,CAAC,2CAA2C,EAAE,QAAQ,CAAC;IACvD,CAAC,yBAAyB,EAAE,QAAQ,CAAC;IACrC,CAAC,8BAA8B,EAAE,WAAW,CAAC;IAC7C,CAAC,gCAAgC,EAAE,UAAU,CAAC;IAC9C,CAAC,kEAAkE,EAAE,KAAK,CAAC;CAC5E,CAAC;AACF,MAAM,OAAO,GAAG,6EAA6E,CAAC;AAE9F,MAAM,IAAI,GAAoB;IAC5B,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE;QAChC,QAAQ,EAAE,EAAE,MAAM,EAAE,sDAAsD,EAAE;QAC5E,MAAM,EAAE,EAAE;KACX;IACD,MAAM,CAAC,OAAO;QACZ,MAAM,KAAK,GAAG,CAAC,IAAS,EAAE,KAAa,EAAE,IAAa,EAAE,EAAE;YACxD,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,QAAQ,EAAE,CAAC;gBAClC,IAAI,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;oBACnB,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;oBAC9D,OAAO;gBACT,CAAC;YACH,CAAC;YACD,+BAA+B;YAC/B,IAAI,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,MAAM,IAAI,EAAE,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC9G,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;YACtE,CAAC;QACH,CAAC,CAAC;QACF,OAAO;YACL,OAAO,CAAC,IAAS;gBACf,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ;oBAAE,OAAO;gBAC3C,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;gBACtB,MAAM,IAAI,GACR,CAAC,EAAE,IAAI,KAAK,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI;oBAC3C,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC;wBACtD,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI;4BAC3D,CAAC,CAAC,SAAS,CAAC;gBACpB,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAChC,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC;AACF,eAAe,IAAI,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const SQL_RE = /\b(select|insert|update|delete)\b[\s\S]*\b(from|into|set|where)\b/i;
|
|
2
|
+
function isSqlish(node) {
|
|
3
|
+
if (!node)
|
|
4
|
+
return false;
|
|
5
|
+
if (node.type === 'Literal' && typeof node.value === 'string')
|
|
6
|
+
return SQL_RE.test(node.value);
|
|
7
|
+
if (node.type === 'TemplateLiteral')
|
|
8
|
+
return node.quasis.some((q) => SQL_RE.test(q.value.raw));
|
|
9
|
+
if (node.type === 'BinaryExpression')
|
|
10
|
+
return isSqlish(node.left) || isSqlish(node.right);
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const rule = {
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: { description: '禁止字符串拼接 SQL' },
|
|
17
|
+
messages: {
|
|
18
|
+
concat: '[CAGE-SQL-001] 不要拼接 SQL 字符串,改用 sql 标签模板或查询构建器(值会自动参数化)',
|
|
19
|
+
raw: '[CAGE-SQL-002] sql.raw() 只能接收字面量',
|
|
20
|
+
},
|
|
21
|
+
schema: [],
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
return {
|
|
25
|
+
TemplateLiteral(node) {
|
|
26
|
+
if (node.parent?.type === 'TaggedTemplateExpression')
|
|
27
|
+
return; // sql`...` 是安全的
|
|
28
|
+
if (node.expressions.length > 0 && isSqlish(node))
|
|
29
|
+
context.report({ node, messageId: 'concat' });
|
|
30
|
+
},
|
|
31
|
+
BinaryExpression(node) {
|
|
32
|
+
if (node.operator !== '+' || node.parent?.type === 'BinaryExpression')
|
|
33
|
+
return;
|
|
34
|
+
if (isSqlish(node) && (node.left.type !== 'Literal' || node.right.type !== 'Literal'))
|
|
35
|
+
context.report({ node, messageId: 'concat' });
|
|
36
|
+
},
|
|
37
|
+
CallExpression(node) {
|
|
38
|
+
const c = node.callee;
|
|
39
|
+
if (c.type === 'MemberExpression' && c.object?.name === 'sql' && c.property?.name === 'raw') {
|
|
40
|
+
const a = node.arguments[0];
|
|
41
|
+
const literal = a && (a.type === 'Literal' || (a.type === 'TemplateLiteral' && a.expressions.length === 0));
|
|
42
|
+
if (!literal)
|
|
43
|
+
context.report({ node, messageId: 'raw' });
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
export default rule;
|
|
50
|
+
//# sourceMappingURL=no-sql-concat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-sql-concat.js","sourceRoot":"","sources":["../../src/rules/no-sql-concat.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,GAAG,oEAAoE,CAAC;AAEpF,SAAS,QAAQ,CAAC,IAAS;IACzB,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9F,IAAI,IAAI,CAAC,IAAI,KAAK,iBAAiB;QAAE,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IACnG,IAAI,IAAI,CAAC,IAAI,KAAK,kBAAkB;QAAE,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzF,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,IAAI,GAAoB;IAC5B,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,EAAE,WAAW,EAAE,aAAa,EAAE;QACpC,QAAQ,EAAE;YACR,MAAM,EAAE,wDAAwD;YAChE,GAAG,EAAE,kCAAkC;SACxC;QACD,MAAM,EAAE,EAAE;KACX;IACD,MAAM,CAAC,OAAO;QACZ,OAAO;YACL,eAAe,CAAC,IAAS;gBACvB,IAAI,IAAI,CAAC,MAAM,EAAE,IAAI,KAAK,0BAA0B;oBAAE,OAAO,CAAC,gBAAgB;gBAC9E,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC;oBAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC;YACnG,CAAC;YACD,gBAAgB,CAAC,IAAS;gBACxB,IAAI,IAAI,CAAC,QAAQ,KAAK,GAAG,IAAI,IAAI,CAAC,MAAM,EAAE,IAAI,KAAK,kBAAkB;oBAAE,OAAO;gBAC9E,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC;oBAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC;YACvI,CAAC;YACD,cAAc,CAAC,IAAS;gBACtB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;gBACtB,IAAI,CAAC,CAAC,IAAI,KAAK,kBAAkB,IAAI,CAAC,CAAC,MAAM,EAAE,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,QAAQ,EAAE,IAAI,KAAK,KAAK,EAAE,CAAC;oBAC5F,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;oBAC5B,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,iBAAiB,IAAI,CAAC,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC;oBAC5G,IAAI,CAAC,OAAO;wBAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;gBAC3D,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC;AACF,eAAe,IAAI,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const rule = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: { description: 'unsafe(...) 必须写明 reason 和 owner' },
|
|
5
|
+
messages: { missing: '[CAGE-UNSAFE-001] unsafe 必须写明 reason 和 owner(详见 docs/errors/CAGE-UNSAFE-001.md)' },
|
|
6
|
+
schema: [],
|
|
7
|
+
},
|
|
8
|
+
create(context) {
|
|
9
|
+
return {
|
|
10
|
+
CallExpression(node) {
|
|
11
|
+
if (node.callee.type !== 'Identifier' || node.callee.name !== 'unsafe')
|
|
12
|
+
return;
|
|
13
|
+
const arg = node.arguments[0];
|
|
14
|
+
const props = arg?.type === 'ObjectExpression' ? arg.properties : [];
|
|
15
|
+
const val = (k) => props.find((p) => (p.key?.name ?? p.key?.value) === k)?.value;
|
|
16
|
+
const filled = (k) => {
|
|
17
|
+
const v = val(k);
|
|
18
|
+
return !!v && !(v.type === 'Literal' && String(v.value).trim() === '');
|
|
19
|
+
};
|
|
20
|
+
if (!filled('reason') || !filled('owner'))
|
|
21
|
+
context.report({ node, messageId: 'missing' });
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
export default rule;
|
|
27
|
+
//# sourceMappingURL=unsafe-needs-reason.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unsafe-needs-reason.js","sourceRoot":"","sources":["../../src/rules/unsafe-needs-reason.ts"],"names":[],"mappings":"AAGA,MAAM,IAAI,GAAoB;IAC5B,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,EAAE,WAAW,EAAE,iCAAiC,EAAE;QACxD,QAAQ,EAAE,EAAE,OAAO,EAAE,iFAAiF,EAAE;QACxG,MAAM,EAAE,EAAE;KACX;IACD,MAAM,CAAC,OAAO;QACZ,OAAO;YACL,cAAc,CAAC,IAAS;gBACtB,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ;oBAAE,OAAO;gBAC/E,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;gBAC9B,MAAM,KAAK,GAAU,GAAG,EAAE,IAAI,KAAK,kBAAkB,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC5E,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC;gBACzF,MAAM,MAAM,GAAG,CAAC,CAAS,EAAE,EAAE;oBAC3B,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;oBACjB,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;gBACzE,CAAC,CAAC;gBACF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;oBAAE,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC;YAC5F,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC;AACF,eAAe,IAAI,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yss123/cage-lint",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "dependency-cruiser 规则与 ESLint 插件",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./dependency-cruiser.cjs": "./dependency-cruiser.cjs"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"dependency-cruiser.cjs"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@typescript-eslint/parser": "^8.42.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"eslint": "^9.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"eslint": "^9.35.0"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/shunshunyue/cage.git",
|
|
32
|
+
"directory": "packages/lint"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/shunshunyue/cage#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/shunshunyue/cage/issues"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"registry": "https://registry.npmjs.org/"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=24"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.json",
|
|
47
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
48
|
+
"test": "vitest run"
|
|
49
|
+
}
|
|
6
50
|
}
|