@af-mobile/eslint-plugin 2.0.4 → 2.2.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/index.js +111 -95
- package/package.json +35 -31
- package/rules/atomic-duplicate.js +68 -60
- package/rules/k-for-require-key.js +33 -0
- package/rules/k-no-bare-and.js +31 -0
- package/rules/k-no-object-interpolation.js +31 -0
- package/rules/no-arbitrary-value.js +52 -52
- package/rules/no-emoji-icon.js +69 -0
- package/rules/no-inline-style.js +129 -127
- package/rules/no-recipe-break.js +59 -59
- package/rules/no-register-all.js +31 -31
- package/rules/no-tailwind-syntax.js +31 -31
- package/rules/no-token-modification.js +92 -92
- package/rules/no-variant-conflict.js +83 -72
- package/rules/prefer-component.js +58 -58
- package/rules/token-whitelist.js +84 -84
- package/rules/wc-aria-required.js +60 -60
- package/rules/wc-block-no-internal-ref.js +60 -60
- package/rules/wc-block-props-count.js +48 -48
- package/rules/wc-block-states.js +64 -64
- package/rules/wc-block-variant-enum.js +83 -83
- package/rules/wc-cleanup.js +75 -75
- package/rules/wc-event-naming.js +60 -60
- package/rules/wc-light-no-style.js +87 -87
- package/rules/wc-part-naming.js +33 -33
- package/rules/wc-shadow-use-token.js +72 -72
- package/utils/aria-requirements.json +101 -101
- package/utils/helpers.js +106 -84
- package/utils/whitelist-v1.json +41 -1
package/index.js
CHANGED
|
@@ -1,95 +1,111 @@
|
|
|
1
|
-
// af-mobile UI —— eslint-plugin-af-mobile 入口(ESLint 9 flat config 兼容)
|
|
2
|
-
//
|
|
3
|
-
// (definePage 消费端 7 条规则已随 definePage 全局单例移除)
|
|
4
|
-
import noTokenModification from './rules/no-token-modification.js';
|
|
5
|
-
import noInlineStyle from './rules/no-inline-style.js';
|
|
6
|
-
import tokenWhitelist from './rules/token-whitelist.js';
|
|
7
|
-
import noRecipeBreak from './rules/no-recipe-break.js';
|
|
8
|
-
import noVariantConflict from './rules/no-variant-conflict.js';
|
|
9
|
-
import noArbitraryValue from './rules/no-arbitrary-value.js';
|
|
10
|
-
import noTailwindSyntax from './rules/no-tailwind-syntax.js';
|
|
11
|
-
import preferComponent from './rules/prefer-component.js';
|
|
12
|
-
import atomicDuplicate from './rules/atomic-duplicate.js';
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
import
|
|
23
|
-
import
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
'token-
|
|
34
|
-
'no-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
|
|
41
|
-
'
|
|
42
|
-
'
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
|
|
46
|
-
'wc-
|
|
47
|
-
|
|
48
|
-
'
|
|
49
|
-
'wc-
|
|
50
|
-
'wc-
|
|
51
|
-
'wc-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
//
|
|
73
|
-
'af-mobile/no-
|
|
74
|
-
'af-mobile/
|
|
75
|
-
|
|
76
|
-
// L3
|
|
77
|
-
|
|
78
|
-
'af-mobile/
|
|
79
|
-
'af-mobile/
|
|
80
|
-
'af-mobile/
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
'af-mobile/
|
|
84
|
-
|
|
85
|
-
'af-mobile/
|
|
86
|
-
'af-mobile/
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
'af-mobile/
|
|
90
|
-
'af-mobile/wc-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
1
|
+
// af-mobile UI —— eslint-plugin-af-mobile 入口(ESLint 9 flat config 兼容)
|
|
2
|
+
// 24 条规则:L1(2) + L2(8) + L3(6) + L3.5(5) + k(3)
|
|
3
|
+
// (definePage 消费端 7 条规则已随 definePage 全局单例移除)
|
|
4
|
+
import noTokenModification from './rules/no-token-modification.js';
|
|
5
|
+
import noInlineStyle from './rules/no-inline-style.js';
|
|
6
|
+
import tokenWhitelist from './rules/token-whitelist.js';
|
|
7
|
+
import noRecipeBreak from './rules/no-recipe-break.js';
|
|
8
|
+
import noVariantConflict from './rules/no-variant-conflict.js';
|
|
9
|
+
import noArbitraryValue from './rules/no-arbitrary-value.js';
|
|
10
|
+
import noTailwindSyntax from './rules/no-tailwind-syntax.js';
|
|
11
|
+
import preferComponent from './rules/prefer-component.js';
|
|
12
|
+
import atomicDuplicate from './rules/atomic-duplicate.js';
|
|
13
|
+
import noEmojiIcon from './rules/no-emoji-icon.js';
|
|
14
|
+
import noRegisterAll from './rules/no-register-all.js';
|
|
15
|
+
import wcLightNoStyle from './rules/wc-light-no-style.js';
|
|
16
|
+
import wcShadowUseToken from './rules/wc-shadow-use-token.js';
|
|
17
|
+
import wcPartNaming from './rules/wc-part-naming.js';
|
|
18
|
+
import wcEventNaming from './rules/wc-event-naming.js';
|
|
19
|
+
import wcAriaRequired from './rules/wc-aria-required.js';
|
|
20
|
+
import wcCleanup from './rules/wc-cleanup.js';
|
|
21
|
+
import wcBlockNoInternalRef from './rules/wc-block-no-internal-ref.js';
|
|
22
|
+
import wcBlockPropsCount from './rules/wc-block-props-count.js';
|
|
23
|
+
import wcBlockStates from './rules/wc-block-states.js';
|
|
24
|
+
import wcBlockVariantEnum from './rules/wc-block-variant-enum.js';
|
|
25
|
+
import kNoBareAnd from './rules/k-no-bare-and.js';
|
|
26
|
+
import kNoObjectInterpolation from './rules/k-no-object-interpolation.js';
|
|
27
|
+
import kForRequireKey from './rules/k-for-require-key.js';
|
|
28
|
+
|
|
29
|
+
const plugin = {
|
|
30
|
+
meta: { name: 'eslint-plugin-af-mobile', version: '2.1.0' },
|
|
31
|
+
rules: {
|
|
32
|
+
// L1(2 条,全部 error;tokens-css-locked 由 CODEOWNERS + 分支保护,非 ESLint 规则)
|
|
33
|
+
'no-token-modification': noTokenModification,
|
|
34
|
+
'no-inline-style': noInlineStyle,
|
|
35
|
+
// L1-3 tokens-css-locked 是 CODEOWNERS + 分支保护,非 ESLint 规则
|
|
36
|
+
// L2(8 条:4 error + 4 warn)
|
|
37
|
+
'token-whitelist': tokenWhitelist,
|
|
38
|
+
'no-recipe-break': noRecipeBreak,
|
|
39
|
+
'no-variant-conflict': noVariantConflict,
|
|
40
|
+
'no-arbitrary-value': noArbitraryValue,
|
|
41
|
+
'no-tailwind-syntax': noTailwindSyntax,
|
|
42
|
+
'prefer-component': preferComponent,
|
|
43
|
+
'atomic-duplicate': atomicDuplicate,
|
|
44
|
+
'no-emoji-icon': noEmojiIcon,
|
|
45
|
+
// L3(6 条:4 error + 2 warn)
|
|
46
|
+
'wc-light-no-style': wcLightNoStyle,
|
|
47
|
+
'wc-shadow-use-token': wcShadowUseToken,
|
|
48
|
+
'wc-part-naming': wcPartNaming,
|
|
49
|
+
'wc-event-naming': wcEventNaming,
|
|
50
|
+
'wc-aria-required': wcAriaRequired,
|
|
51
|
+
'wc-cleanup': wcCleanup,
|
|
52
|
+
// L3.5(5 条:Block 层约束 + register 引导)
|
|
53
|
+
'no-register-all': noRegisterAll,
|
|
54
|
+
'wc-block-no-internal-ref': wcBlockNoInternalRef,
|
|
55
|
+
'wc-block-props-count': wcBlockPropsCount,
|
|
56
|
+
'wc-block-states': wcBlockStates,
|
|
57
|
+
'wc-block-variant-enum': wcBlockVariantEnum,
|
|
58
|
+
// k 层(3 条:D-001=B 应用层推广的配套约束;仅对从 @af-mobile/ui/k 导入的 html/For 生效,
|
|
59
|
+
// 主包同名 API 语义不同不受约束,规则内经 collectKImports 判定)
|
|
60
|
+
'k-no-bare-and': kNoBareAnd,
|
|
61
|
+
'k-no-object-interpolation': kNoObjectInterpolation,
|
|
62
|
+
'k-for-require-key': kForRequireKey,
|
|
63
|
+
},
|
|
64
|
+
configs: {},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// recommended 配置:按设计文档 L4 §3.2-3.4 的 severity 启用
|
|
68
|
+
Object.assign(plugin.configs, {
|
|
69
|
+
recommended: {
|
|
70
|
+
plugins: { 'af-mobile': plugin },
|
|
71
|
+
rules: {
|
|
72
|
+
// L1 error
|
|
73
|
+
'af-mobile/no-token-modification': 'error',
|
|
74
|
+
'af-mobile/no-inline-style': 'error',
|
|
75
|
+
// L2 error
|
|
76
|
+
// extraComponents: af-data 属 L3.5 Block 层数据源元素(非 L3 注册组件,不在 whitelist-v1.json),
|
|
77
|
+
// 但 AI 生成的页面代码(createPage/:bind 体系)需要能使用 <af-data>,故在规则级放行
|
|
78
|
+
'af-mobile/token-whitelist': ['error', { extraComponents: ['af-data'] }],
|
|
79
|
+
'af-mobile/no-recipe-break': 'error',
|
|
80
|
+
'af-mobile/no-arbitrary-value': 'error',
|
|
81
|
+
'af-mobile/no-tailwind-syntax': 'error',
|
|
82
|
+
// L2 warn
|
|
83
|
+
'af-mobile/no-variant-conflict': 'warn',
|
|
84
|
+
'af-mobile/prefer-component': 'warn',
|
|
85
|
+
'af-mobile/atomic-duplicate': 'warn',
|
|
86
|
+
'af-mobile/no-emoji-icon': 'warn',
|
|
87
|
+
// L3 error
|
|
88
|
+
'af-mobile/wc-light-no-style': 'error',
|
|
89
|
+
'af-mobile/wc-shadow-use-token': 'error',
|
|
90
|
+
'af-mobile/wc-event-naming': 'error',
|
|
91
|
+
'af-mobile/wc-aria-required': 'error',
|
|
92
|
+
// L3 warn
|
|
93
|
+
'af-mobile/wc-part-naming': 'warn',
|
|
94
|
+
'af-mobile/wc-cleanup': 'warn',
|
|
95
|
+
// L3.5 error(Block 层约束)
|
|
96
|
+
'af-mobile/wc-block-no-internal-ref': 'error',
|
|
97
|
+
'af-mobile/wc-block-props-count': 'error',
|
|
98
|
+
'af-mobile/wc-block-states': 'error',
|
|
99
|
+
// L3.5 error(按需引入铁律:registerAll 全量注册 = 全局引入,一律禁止)
|
|
100
|
+
'af-mobile/no-register-all': 'error',
|
|
101
|
+
'af-mobile/wc-block-variant-enum': 'warn',
|
|
102
|
+
// k 层(k 渲染/应用层:裸 && 渲染 "false"、对象插值 "[object Object]" 是静默 bug → error;
|
|
103
|
+
// For 缺 key 对对象列表破坏 keyed 复用但原始值列表合法 → warn)
|
|
104
|
+
'af-mobile/k-no-bare-and': 'error',
|
|
105
|
+
'af-mobile/k-no-object-interpolation': 'error',
|
|
106
|
+
'af-mobile/k-for-require-key': 'warn',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export default plugin;
|
package/package.json
CHANGED
|
@@ -1,31 +1,35 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@af-mobile/eslint-plugin",
|
|
3
|
-
"version": "2.0
|
|
4
|
-
"description": "af-mobile UI ESLint plugin —
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./index.js",
|
|
7
|
-
"module": "./index.js",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": "./index.js"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"index.js",
|
|
13
|
-
"rules/",
|
|
14
|
-
"utils/"
|
|
15
|
-
],
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"postcss": "^8.4.0"
|
|
18
|
-
},
|
|
19
|
-
"peerDependencies": {
|
|
20
|
-
"eslint": ">=9.0.0"
|
|
21
|
-
},
|
|
22
|
-
"keywords": [
|
|
23
|
-
"eslint",
|
|
24
|
-
"eslintplugin",
|
|
25
|
-
"eslint-plugin",
|
|
26
|
-
"web-components",
|
|
27
|
-
"design-system",
|
|
28
|
-
"af-mobile"
|
|
29
|
-
],
|
|
30
|
-
"license": "MIT"
|
|
31
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@af-mobile/eslint-plugin",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "af-mobile UI ESLint plugin — 24 rules (L1/L2/L3/L3.5/k) enforcing design-system + Web Component + k-layer constraints",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"module": "./index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js",
|
|
13
|
+
"rules/",
|
|
14
|
+
"utils/"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"postcss": "^8.4.0"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"eslint": ">=9.0.0"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"eslint",
|
|
24
|
+
"eslintplugin",
|
|
25
|
+
"eslint-plugin",
|
|
26
|
+
"web-components",
|
|
27
|
+
"design-system",
|
|
28
|
+
"af-mobile"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/soulor8908/af-mobile.git"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,60 +1,68 @@
|
|
|
1
|
-
// L2-7 af-mobile/atomic-duplicate(warn,可自动修)
|
|
2
|
-
// 检测:同一 class 属性内出现两个同属性原子(如 p-4 p-2),保留最后一个
|
|
3
|
-
import { extractAllClassLists } from '../utils/helpers.js';
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
1
|
+
// L2-7 af-mobile/atomic-duplicate(warn,可自动修)
|
|
2
|
+
// 检测:同一 class 属性内出现两个同属性原子(如 p-4 p-2),保留最后一个
|
|
3
|
+
import { extractAllClassLists } from '../utils/helpers.js';
|
|
4
|
+
|
|
5
|
+
// 字重类与字号类共享 t- 前缀但作用于不同属性(font-weight vs font-size),必须分桶判定:
|
|
6
|
+
// t-xl t-b 是"大字号+粗体"的合法排版组合,不是字号重复
|
|
7
|
+
// (v1.6.1 修复:旧前缀正则曾把两者混为 font-size,autofix 直接删掉字号类,破坏视觉强调)
|
|
8
|
+
const FONT_SIZE = new Set(['t-display', 't-xs', 't-sm', 't-md', 't-lg', 't-xl']);
|
|
9
|
+
const FONT_WEIGHT = new Set(['t-b', 't-m', 't-semibold']);
|
|
10
|
+
|
|
11
|
+
// 属性桶:p-* → padding, m-* → margin, t-* → font-size, r-* → border-radius, 字重类 → font-weight
|
|
12
|
+
function getPropPrefix(cls) {
|
|
13
|
+
if (FONT_WEIGHT.has(cls)) return 'fw';
|
|
14
|
+
if (FONT_SIZE.has(cls)) return 't';
|
|
15
|
+
const m = cls.match(/^([pmtr])-(.+)$/);
|
|
16
|
+
return m ? m[1] : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const PROP_LABELS = { p: 'padding', m: 'margin', t: 'font-size', r: 'border-radius', fw: 'font-weight' };
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
meta: {
|
|
23
|
+
type: 'suggestion',
|
|
24
|
+
docs: { description: '检测同属性原子类重复,保留最后一个' },
|
|
25
|
+
schema: [],
|
|
26
|
+
messages: {
|
|
27
|
+
duplicate: "Duplicate {{prop}}: '{{a}}' is overwritten by '{{b}}'. Keep only '{{b}}'",
|
|
28
|
+
},
|
|
29
|
+
fixable: 'code',
|
|
30
|
+
},
|
|
31
|
+
create(context) {
|
|
32
|
+
function checkClasses(classes, node, raw) {
|
|
33
|
+
const seen = new Map(); // prefix → { cls, index }
|
|
34
|
+
for (let i = 0; i < classes.length; i++) {
|
|
35
|
+
const cls = classes[i];
|
|
36
|
+
const prefix = getPropPrefix(cls);
|
|
37
|
+
if (!prefix) continue;
|
|
38
|
+
if (seen.has(prefix)) {
|
|
39
|
+
const prev = seen.get(prefix);
|
|
40
|
+
context.report({
|
|
41
|
+
node,
|
|
42
|
+
messageId: 'duplicate',
|
|
43
|
+
data: { prop: PROP_LABELS[prefix] || prefix, a: prev.cls, b: cls },
|
|
44
|
+
fix(fixer) {
|
|
45
|
+
// 删除被覆盖的 class + 其后的空格(保留的类在后面,必有空格可吃)
|
|
46
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
47
|
+
const text = sourceCode.getText(node);
|
|
48
|
+
const idx = text.indexOf(raw);
|
|
49
|
+
if (idx < 0) return null;
|
|
50
|
+
const classIdx = idx + raw.indexOf(prev.cls);
|
|
51
|
+
const before = text.slice(0, classIdx);
|
|
52
|
+
const after = text.slice(classIdx + prev.cls.length).replace(/^\s+/, '');
|
|
53
|
+
return fixer.replaceText(node, before + after);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
seen.set(prefix, { cls, index: i });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function checkString(str, node) {
|
|
61
|
+
for (const { classes, raw } of extractAllClassLists(str)) checkClasses(classes, node, raw);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
Literal(node) { if (typeof node.value === 'string') checkString(node.value, node); },
|
|
65
|
+
TemplateElement(node) { if (node.value?.raw) checkString(node.value.raw, node); },
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// k-3 af-mobile/k-for-require-key(warn)
|
|
2
|
+
// For 省略 key 时以项本身为键:原始值列表语义正确;对象列表以引用为键——
|
|
3
|
+
// 数据源每次 set 新数组即整行重建,keyed 复用失效(src/k/README.md 词表卡 key 边界)
|
|
4
|
+
// 修正:显式传 key: 'id'(稳定字段名);纯原始值列表可显式关闭本规则
|
|
5
|
+
// 仅对从 k 入口(@af-mobile/ui/k)导入的 For 生效
|
|
6
|
+
import { collectKImports } from '../utils/helpers.js';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'suggestion',
|
|
11
|
+
docs: { description: 'k For 建议显式传 key(对象项省略 key 以引用为键,引用变则整行重建)' },
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
forKeyMissing: 'For is missing "key" — object items are keyed by reference when omitted, so every source change rebuilds all rows; pass key: "id" (primitive lists may disable this rule)',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
const kNames = collectKImports(context.sourceCode.ast.body);
|
|
19
|
+
if (kNames.For.size === 0) return {};
|
|
20
|
+
return {
|
|
21
|
+
CallExpression(node) {
|
|
22
|
+
if (node.callee.type !== 'Identifier' || !kNames.For.has(node.callee.name)) return;
|
|
23
|
+
const arg = node.arguments[0];
|
|
24
|
+
if (!arg || arg.type !== 'ObjectExpression') return;
|
|
25
|
+
const hasKey = arg.properties.some((p) => p.type === 'Property' && (
|
|
26
|
+
(p.key.type === 'Identifier' && p.key.name === 'key')
|
|
27
|
+
|| (p.key.type === 'Literal' && p.key.value === 'key')
|
|
28
|
+
));
|
|
29
|
+
if (!hasKey) context.report({ node: arg, messageId: 'forKeyMissing' });
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// k-1 af-mobile/k-no-bare-and(error)
|
|
2
|
+
// k 的 html`` 子位渲染 String(val):裸 && 在假值时把字面量 "false" 渲染成文本节点
|
|
3
|
+
// (JSX `cond && <X/>` 的幻觉写法;k 无"假值不渲染"语义)
|
|
4
|
+
// 修正:三元 `${cond ? x : null}`(null → 空串)或 Show({ when, kids })
|
|
5
|
+
// 仅对从 k 入口(@af-mobile/ui/k)导入的 html 生效;主包 html``(字符串拼接)不受约束
|
|
6
|
+
import { collectKImports } from '../utils/helpers.js';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'problem',
|
|
11
|
+
docs: { description: 'k html`` 子位禁止裸 && 表达式(假值渲染 "false" 文本)' },
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
bareAnd: 'Bare && in k html`` renders literal text "false" when falsy — use a ternary `${cond ? x : null}` or Show({ when, kids })',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
const kNames = collectKImports(context.sourceCode.ast.body);
|
|
19
|
+
if (kNames.html.size === 0) return {};
|
|
20
|
+
return {
|
|
21
|
+
TaggedTemplateExpression(node) {
|
|
22
|
+
if (node.tag.type !== 'Identifier' || !kNames.html.has(node.tag.name)) return;
|
|
23
|
+
for (const expr of node.quasi.expressions) {
|
|
24
|
+
if (expr.type === 'LogicalExpression' && expr.operator === '&&') {
|
|
25
|
+
context.report({ node: expr, messageId: 'bareAnd' });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// k-2 af-mobile/k-no-object-interpolation(error)
|
|
2
|
+
// k 的 html`` 插值经 String(val):对象字面量渲染成 "[object Object]" 文本
|
|
3
|
+
// 典型来源:主包 html`` 的可信 HTML 语法 `${{ raw: '<b>x</b>' }}` 在 k 不存在(JSX/主包双重幻觉)
|
|
4
|
+
// 修正:插 signal/getter、字符串或 DOM 节点;数组合法(bindKids 展开为多节点)不报
|
|
5
|
+
// 仅对从 k 入口(@af-mobile/ui/k)导入的 html 生效;主包 html`` 的 { raw } 语法不受约束
|
|
6
|
+
import { collectKImports } from '../utils/helpers.js';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'problem',
|
|
11
|
+
docs: { description: 'k html`` 插值禁止对象字面量(渲染 "[object Object]";k 无 { raw } 语法)' },
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
objectInterpolation: 'Object literal in k html`` renders "[object Object]" — k has no { raw } syntax; interpolate a signal/getter, string, or DOM node instead (arrays are legal, they expand to multiple nodes)',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
const kNames = collectKImports(context.sourceCode.ast.body);
|
|
19
|
+
if (kNames.html.size === 0) return {};
|
|
20
|
+
return {
|
|
21
|
+
TaggedTemplateExpression(node) {
|
|
22
|
+
if (node.tag.type !== 'Identifier' || !kNames.html.has(node.tag.name)) return;
|
|
23
|
+
for (const expr of node.quasi.expressions) {
|
|
24
|
+
if (expr.type === 'ObjectExpression') {
|
|
25
|
+
context.report({ node: expr, messageId: 'objectInterpolation' });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
// L2-4 af-mobile/no-arbitrary-value(error,部分可自动修)
|
|
2
|
-
// 检测:class 含 [xxx] 任意值语法 / 档位越界(如 p-9)
|
|
3
|
-
import { extractAllClassLists, ALL_CLASSES } from '../utils/helpers.js';
|
|
4
|
-
|
|
5
|
-
// 合法档位
|
|
6
|
-
const RANGES = {
|
|
7
|
-
'p': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '10'],
|
|
8
|
-
'm': ['0', '1', '2', '3', '4', '5', '6'],
|
|
9
|
-
't': ['xs', 'sm', 'md', 'lg', 'xl'],
|
|
10
|
-
'r': ['s', 'm', 'l', 'f'],
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export default {
|
|
14
|
-
meta: {
|
|
15
|
-
type: 'problem',
|
|
16
|
-
docs: { description: '禁止任意值语法和档位越界' },
|
|
17
|
-
schema: [],
|
|
18
|
-
messages: {
|
|
19
|
-
arbitrary: "'{{name}}' arbitrary value syntax forbidden; use closest atomic or extend recipes.project.css",
|
|
20
|
-
outOfRange: "'{{name}}' out of range ({{valid}}); use closest atomic",
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
create(context) {
|
|
24
|
-
function checkClass(cls, node) {
|
|
25
|
-
// 白名单内的 class 直接放行(如 t-center/t-b 是合法原子类,非档位)
|
|
26
|
-
if (ALL_CLASSES.has(cls)) return;
|
|
27
|
-
// [xxx] 任意值
|
|
28
|
-
if (/[\[\]]/.test(cls)) {
|
|
29
|
-
context.report({ node, messageId: 'arbitrary', data: { name: cls } });
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
// 档位越界:前缀-档位
|
|
33
|
-
const m = cls.match(/^([pmtr])-(.+)$/);
|
|
34
|
-
if (m) {
|
|
35
|
-
const [, prefix, val] = m;
|
|
36
|
-
const valid = RANGES[prefix.toUpperCase()] || RANGES[prefix];
|
|
37
|
-
if (valid && !valid.includes(val)) {
|
|
38
|
-
context.report({ node, messageId: 'outOfRange', data: { name: cls, valid: valid.join('/') } });
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
function checkString(str, node) {
|
|
43
|
-
for (const { classes } of extractAllClassLists(str)) {
|
|
44
|
-
for (const cls of classes) checkClass(cls, node);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return {
|
|
48
|
-
Literal(node) { if (typeof node.value === 'string') checkString(node.value, node); },
|
|
49
|
-
TemplateElement(node) { if (node.value?.raw) checkString(node.value.raw, node); },
|
|
50
|
-
};
|
|
51
|
-
},
|
|
52
|
-
};
|
|
1
|
+
// L2-4 af-mobile/no-arbitrary-value(error,部分可自动修)
|
|
2
|
+
// 检测:class 含 [xxx] 任意值语法 / 档位越界(如 p-9)
|
|
3
|
+
import { extractAllClassLists, ALL_CLASSES } from '../utils/helpers.js';
|
|
4
|
+
|
|
5
|
+
// 合法档位
|
|
6
|
+
const RANGES = {
|
|
7
|
+
'p': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '10'],
|
|
8
|
+
'm': ['0', '1', '2', '3', '4', '5', '6'],
|
|
9
|
+
't': ['xs', 'sm', 'md', 'lg', 'xl'],
|
|
10
|
+
'r': ['s', 'm', 'l', 'f'],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: { description: '禁止任意值语法和档位越界' },
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
arbitrary: "'{{name}}' arbitrary value syntax forbidden; use closest atomic or extend recipes.project.css",
|
|
20
|
+
outOfRange: "'{{name}}' out of range ({{valid}}); use closest atomic",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
function checkClass(cls, node) {
|
|
25
|
+
// 白名单内的 class 直接放行(如 t-center/t-b 是合法原子类,非档位)
|
|
26
|
+
if (ALL_CLASSES.has(cls)) return;
|
|
27
|
+
// [xxx] 任意值
|
|
28
|
+
if (/[\[\]]/.test(cls)) {
|
|
29
|
+
context.report({ node, messageId: 'arbitrary', data: { name: cls } });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// 档位越界:前缀-档位
|
|
33
|
+
const m = cls.match(/^([pmtr])-(.+)$/);
|
|
34
|
+
if (m) {
|
|
35
|
+
const [, prefix, val] = m;
|
|
36
|
+
const valid = RANGES[prefix.toUpperCase()] || RANGES[prefix];
|
|
37
|
+
if (valid && !valid.includes(val)) {
|
|
38
|
+
context.report({ node, messageId: 'outOfRange', data: { name: cls, valid: valid.join('/') } });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function checkString(str, node) {
|
|
43
|
+
for (const { classes } of extractAllClassLists(str)) {
|
|
44
|
+
for (const cls of classes) checkClass(cls, node);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
Literal(node) { if (typeof node.value === 'string') checkString(node.value, node); },
|
|
49
|
+
TemplateElement(node) { if (node.value?.raw) checkString(node.value.raw, node); },
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|