eslint-plugin-light 1.0.21 → 1.0.23
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/CHANGELOG.md +16 -0
- package/README.md +71 -2
- package/lib/configs/recommended.js +1 -0
- package/lib/rules/classname-per-line.js +9 -6
- package/lib/rules/img-v-lazy.js +57 -26
- package/lib/rules/json-parse-try-catch.js +9 -3
- package/lib/rules/no-complex-key.js +6 -3
- package/lib/rules/no-complex-style-class.js +83 -0
- package/lib/rules/no-decimal-in-brackets.js +7 -2
- package/lib/rules/no-import-vant.js +1 -1
- package/lib/rules/no-js-file.js +1 -1
- package/lib/rules/no-multiple-script.js +2 -2
- package/lib/rules/no-plus-turn-number.js +6 -3
- package/lib/rules/no-src-imports-in-components.js +1 -1
- package/lib/rules/no-todo-comment.js +4 -4
- package/lib/rules/valid-file-name.js +1 -1
- package/lib/rules/valid-pmd-import.js +2 -2
- package/lib/rules/valid-vue-comp-import.js +1 -1
- package/lib/test/img-v-lazy.js +87 -0
- package/lib/test/index.js +2 -0
- package/lib/test/no-complex-style-class.js +85 -0
- package/lib/utils/parser-services.js +26 -0
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## <small>1.0.23 (2026-04-27)</small>
|
|
2
|
+
|
|
3
|
+
* feat(plugin-light): 三元表达式不修复 ([ecabda4](https://github.com/novlan1/plugin-light/commits/ecabda4))
|
|
4
|
+
* chore: update docs ([84adc98](https://github.com/novlan1/plugin-light/commits/84adc98))
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## <small>1.0.22 (2026-03-24)</small>
|
|
9
|
+
|
|
10
|
+
* feat: add no-complex-style-class rule ([2cc25ee](https://github.com/novlan1/plugin-light/commits/2cc25ee))
|
|
11
|
+
* feat: optmize eslint rules ([b3e85c6](https://github.com/novlan1/plugin-light/commits/b3e85c6))
|
|
12
|
+
* chore: update deps ([6828b0c](https://github.com/novlan1/plugin-light/commits/6828b0c))
|
|
13
|
+
* chore: update package.json ([338b104](https://github.com/novlan1/plugin-light/commits/338b104))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
1
17
|
## <small>1.0.21 (2026-01-04)</small>
|
|
2
18
|
|
|
3
19
|
* feat(eslint-plugin-light): add no-src-imports-in-components rule ([b960951](https://github.com/novlan1/plugin-light/commits/b960951))
|
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<img src="https://img.shields.io/npm/unpacked-size/eslint-plugin-light">
|
|
6
6
|
<img src="https://img.shields.io/npm/v/eslint-plugin-light">
|
|
7
7
|
<img src="https://img.shields.io/npm/l/eslint-plugin-light">
|
|
8
|
-
<img src="https://img.shields.io/
|
|
9
|
-
<img src="https://img.shields.io/
|
|
8
|
+
<img src="https://img.shields.io/badge/TypeScript-strict-blue?logo=typescript&logoColor=white" alt="TypeScript strict">
|
|
9
|
+
<img src="https://img.shields.io/badge/created-2022--01-blue?logo=github&logoColor=white">
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
简单、易用的 ESLint 插件库。
|
|
@@ -801,6 +801,75 @@ bannedPrefixes: ['src', '@/', '~/']
|
|
|
801
801
|
message: '该目录将被发布为独立的 npm 包,请使用相对路径导入'
|
|
802
802
|
```
|
|
803
803
|
|
|
804
|
+
### 4.19. no-complex-style-class
|
|
805
|
+
|
|
806
|
+
禁止在 Vue 模板的 `:style` 或 `:class` 绑定中**直接**使用函数调用、模板字符串等复杂表达式。
|
|
807
|
+
|
|
808
|
+
背景:
|
|
809
|
+
|
|
810
|
+
> uni-app Vue2 小程序中,`:style` 和 `:class` 的**顶层表达式**不支持函数调用和模板字符串等语法。例如 `:style="tool._style(xxx)"` 或 `:class="\`${prefix}-item\`"` 在小程序中会解析失败。
|
|
811
|
+
>
|
|
812
|
+
> 应使用计算属性或字符串拼接 `'' + xxx` 代替。
|
|
813
|
+
>
|
|
814
|
+
> 注意:在数组元素、对象属性值、条件表达式分支中使用函数调用是**允许**的,例如 `:class="[getClass(), 'base']"` 和 `:style="flag ? getStyle() : ''"` 在小程序中可以正常工作。
|
|
815
|
+
|
|
816
|
+
Usage:
|
|
817
|
+
|
|
818
|
+
```js
|
|
819
|
+
// .eslintrc.js
|
|
820
|
+
|
|
821
|
+
module.exports = {
|
|
822
|
+
plugins: [
|
|
823
|
+
'light',
|
|
824
|
+
],
|
|
825
|
+
rules: {
|
|
826
|
+
'light/no-complex-style-class': 2,
|
|
827
|
+
},
|
|
828
|
+
}
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
错误示例:
|
|
832
|
+
|
|
833
|
+
```html
|
|
834
|
+
<!-- ❌ 顶层函数调用 -->
|
|
835
|
+
<div :style="tool._style(xxx)" />
|
|
836
|
+
<div :class="getClass(item)" />
|
|
837
|
+
|
|
838
|
+
<!-- ❌ 顶层模板字符串 -->
|
|
839
|
+
<div :style="`color: ${color}`" />
|
|
840
|
+
<div :class="`${prefix}-item`" />
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
正确示例:
|
|
844
|
+
|
|
845
|
+
```html
|
|
846
|
+
<!-- ✅ 变量引用 -->
|
|
847
|
+
<div :style="myStyle" />
|
|
848
|
+
<div :class="item.className" />
|
|
849
|
+
|
|
850
|
+
<!-- ✅ 对象表达式(值为变量) -->
|
|
851
|
+
<div :style="{ color: myColor }" />
|
|
852
|
+
<div :class="{ active: isActive }" />
|
|
853
|
+
|
|
854
|
+
<!-- ✅ 字面量数组 -->
|
|
855
|
+
<div :class="['a', 'b']" />
|
|
856
|
+
|
|
857
|
+
<!-- ✅ 条件表达式(值为变量) -->
|
|
858
|
+
<div :style="flag ? styleA : styleB" />
|
|
859
|
+
|
|
860
|
+
<!-- ✅ 数组元素中的函数调用 -->
|
|
861
|
+
<div :class="[utils.getClass(classPrefix, 'medium'), tClassImage]" />
|
|
862
|
+
|
|
863
|
+
<!-- ✅ 条件表达式分支中的函数调用 -->
|
|
864
|
+
<div :style="inChat ? imageStyle(item) : ''" />
|
|
865
|
+
|
|
866
|
+
<!-- ✅ 数组 + 条件混合 -->
|
|
867
|
+
<div :class="[classPrefix, inChat ? classPrefix + '--chatting' : '', getFileTypeClass(inChat, files)]" />
|
|
868
|
+
|
|
869
|
+
<!-- ✅ 对象属性值中的函数调用 -->
|
|
870
|
+
<div :style="{ color: getColor() }" />
|
|
871
|
+
```
|
|
872
|
+
|
|
804
873
|
## 5. 更新日志
|
|
805
874
|
|
|
806
875
|
[点此查看](./CHANGELOG.md)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { getParserServices } = require('../utils/parser-services');
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
const DEFAULT_THRESHOLD = 3;
|
|
@@ -26,16 +26,20 @@ module.exports = {
|
|
|
26
26
|
], // 无配置选项
|
|
27
27
|
},
|
|
28
28
|
create(context) {
|
|
29
|
-
|
|
29
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
30
|
+
|
|
31
|
+
const parserServices = getParserServices(context);
|
|
32
|
+
if (!parserServices?.defineTemplateBodyVisitor) {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return parserServices.defineTemplateBodyVisitor({
|
|
30
37
|
'VAttribute[key.name=\'class\']'(node) {
|
|
31
38
|
if (!node.value || node.value.type !== 'VLiteral') return;
|
|
32
39
|
|
|
33
40
|
const options = context.options[0] || {};
|
|
34
41
|
const threshold = options.spelling || DEFAULT_THRESHOLD;
|
|
35
42
|
|
|
36
|
-
|
|
37
|
-
const sourceCode = context.getSourceCode();
|
|
38
|
-
|
|
39
43
|
const text = sourceCode.getText(node.value);
|
|
40
44
|
const classValue = node.value.value.trim();
|
|
41
45
|
|
|
@@ -56,7 +60,6 @@ module.exports = {
|
|
|
56
60
|
loc: node.value.loc,
|
|
57
61
|
message: 'CSS classes should be one per line',
|
|
58
62
|
fix(fixer) {
|
|
59
|
-
const sourceCode = context.getSourceCode();
|
|
60
63
|
const [startQuote, endQuote] = getQuoteChars(sourceCode, node.value);
|
|
61
64
|
|
|
62
65
|
const indentation = ' '.repeat(node.loc.start.column);
|
package/lib/rules/img-v-lazy.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { getParserServices } = require('../utils/parser-services');
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
4
|
meta: {
|
|
@@ -9,10 +9,31 @@ module.exports = {
|
|
|
9
9
|
recommended: true,
|
|
10
10
|
},
|
|
11
11
|
fixable: 'code',
|
|
12
|
+
messages: {
|
|
13
|
+
useVLazy: 'Use v-lazy instead of :src for image loading',
|
|
14
|
+
useVLazyManual: 'Use v-lazy instead of :src for image loading (manual replacement required: conditional or fallback expression detected)',
|
|
15
|
+
},
|
|
12
16
|
schema: [],
|
|
13
17
|
},
|
|
14
18
|
create(context) {
|
|
15
|
-
|
|
19
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
20
|
+
|
|
21
|
+
// 判断一个节点是否含有字符串字面量(递归检查 || 链)
|
|
22
|
+
function hasStringLiteral(n) {
|
|
23
|
+
if (!n) return false;
|
|
24
|
+
if (n.type === 'Literal' && typeof n.value === 'string') return true;
|
|
25
|
+
if (n.type === 'LogicalExpression' && n.operator === '||') {
|
|
26
|
+
return hasStringLiteral(n.left) || hasStringLiteral(n.right);
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const parserServices = getParserServices(context);
|
|
32
|
+
if (!parserServices?.defineTemplateBodyVisitor) {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return parserServices.defineTemplateBodyVisitor({
|
|
16
37
|
'VElement[name=\'img\']'(node) {
|
|
17
38
|
const srcBinding = node.startTag.attributes.find(attr => attr.directive
|
|
18
39
|
&& attr.key.name
|
|
@@ -25,30 +46,40 @@ module.exports = {
|
|
|
25
46
|
&& attr.key.name.name === 'lazy');
|
|
26
47
|
|
|
27
48
|
if (srcBinding && !hasVLazy) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
const expr = srcBinding.value && srcBinding.value.expression;
|
|
50
|
+
|
|
51
|
+
const isConditional = expr && expr.type === 'ConditionalExpression';
|
|
52
|
+
const isLogicalOr = expr && expr.type === 'LogicalExpression' && expr.operator === '||';
|
|
53
|
+
const isLiteral = expr && expr.type === 'Literal';
|
|
54
|
+
|
|
55
|
+
// 三元表达式 → 手动
|
|
56
|
+
// 顶层字符串字面量 → 手动
|
|
57
|
+
// || 且含字符串字面量 → 用反引号自动修复
|
|
58
|
+
// || 且不含字符串字面量 → 正常双引号自动修复
|
|
59
|
+
if (isConditional || isLiteral) {
|
|
60
|
+
context.report({
|
|
61
|
+
node,
|
|
62
|
+
messageId: 'useVLazyManual',
|
|
63
|
+
});
|
|
64
|
+
} else if (isLogicalOr && hasStringLiteral(expr)) {
|
|
65
|
+
context.report({
|
|
66
|
+
node,
|
|
67
|
+
messageId: 'useVLazy',
|
|
68
|
+
fix(fixer) {
|
|
69
|
+
const srcText = sourceCode.getText(expr);
|
|
70
|
+
return fixer.replaceText(srcBinding, `v-lazy=\`${srcText}\``);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
} else {
|
|
74
|
+
context.report({
|
|
75
|
+
node,
|
|
76
|
+
messageId: 'useVLazy',
|
|
77
|
+
fix(fixer) {
|
|
78
|
+
const srcText = sourceCode.getText(expr);
|
|
79
|
+
return fixer.replaceText(srcBinding, `v-lazy="${srcText}"`);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
52
83
|
}
|
|
53
84
|
},
|
|
54
85
|
});
|
|
@@ -10,6 +10,9 @@ module.exports = {
|
|
|
10
10
|
description: desc,
|
|
11
11
|
category: 'Best Practices',
|
|
12
12
|
},
|
|
13
|
+
messages: {
|
|
14
|
+
requireTryCatch: desc,
|
|
15
|
+
},
|
|
13
16
|
},
|
|
14
17
|
schema: [{
|
|
15
18
|
type: 'object',
|
|
@@ -32,12 +35,12 @@ module.exports = {
|
|
|
32
35
|
const calleePropertyName = node.callee?.property?.name;
|
|
33
36
|
|
|
34
37
|
const subNode = node.arguments?.[0];
|
|
35
|
-
const
|
|
38
|
+
const subCalleeObjectName = subNode?.callee?.object?.name;
|
|
36
39
|
const subCalleePropertyName = subNode?.callee?.property?.name;
|
|
37
40
|
|
|
38
41
|
if (calleeObjectName === 'JSON'
|
|
39
42
|
&& calleePropertyName === 'parse'
|
|
40
|
-
&& (strict || !(
|
|
43
|
+
&& (strict || !(subCalleeObjectName === 'JSON' && subCalleePropertyName === 'stringify'))
|
|
41
44
|
) {
|
|
42
45
|
let p = node.parent;
|
|
43
46
|
let hasTryFlg = false;
|
|
@@ -48,7 +51,10 @@ module.exports = {
|
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
if (!hasTryFlg) {
|
|
51
|
-
context.report(
|
|
54
|
+
context.report({
|
|
55
|
+
node,
|
|
56
|
+
messageId: 'requireTryCatch',
|
|
57
|
+
});
|
|
52
58
|
}
|
|
53
59
|
}
|
|
54
60
|
},
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { getParserServices } = require('../utils/parser-services');
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
meta: {
|
|
3
5
|
type: 'problem',
|
|
@@ -14,16 +16,17 @@ module.exports = {
|
|
|
14
16
|
},
|
|
15
17
|
|
|
16
18
|
create(context) {
|
|
17
|
-
const fileName = context.getFilename();
|
|
19
|
+
const fileName = context.filename || context.getFilename();
|
|
18
20
|
if (!fileName.endsWith('.vue')) {
|
|
19
21
|
return {};
|
|
20
22
|
}
|
|
21
|
-
|
|
23
|
+
const parserServices = getParserServices(context);
|
|
24
|
+
if (!parserServices?.defineTemplateBodyVisitor) {
|
|
22
25
|
return {
|
|
23
26
|
};
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
return
|
|
29
|
+
return parserServices.defineTemplateBodyVisitor({
|
|
27
30
|
VAttribute(node) {
|
|
28
31
|
if (!node.key
|
|
29
32
|
|| node.key.type !== 'VDirectiveKey'
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const { getParserServices } = require('../utils/parser-services');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'problem',
|
|
6
|
+
schema: [],
|
|
7
|
+
docs: {
|
|
8
|
+
description: 'vue模板中不要在 :style 或 :class 中使用函数调用、模板字符串等复杂表达式(uni-app 小程序下不支持)',
|
|
9
|
+
},
|
|
10
|
+
messages: {
|
|
11
|
+
funcStyleError: 'Do not use function call in :style binding, use computed property or `\'\' + xxx` instead',
|
|
12
|
+
tplStyleError: 'Do not use template literal in :style binding, use computed property or `\'\' + xxx` instead',
|
|
13
|
+
funcClassError: 'Do not use function call in :class binding, use computed property or `\'\' + xxx` instead',
|
|
14
|
+
tplClassError: 'Do not use template literal in :class binding, use computed property or `\'\' + xxx` instead',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
create(context) {
|
|
19
|
+
const fileName = context.filename || context.getFilename();
|
|
20
|
+
if (!fileName.endsWith('.vue')) {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
const parserServices = getParserServices(context);
|
|
24
|
+
if (!parserServices?.defineTemplateBodyVisitor) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 检查顶层表达式是否为函数调用或模板字符串
|
|
30
|
+
*
|
|
31
|
+
* 只检测直接绑定到 :style/:class 的顶层表达式,不递归检查子表达式。
|
|
32
|
+
* 因为数组元素、对象属性值、条件表达式分支中的函数调用/模板字符串
|
|
33
|
+
* 在 uni-app 小程序中是支持的,例如:
|
|
34
|
+
* :class="[utils.getClass(...), tClassImage]" — 合法
|
|
35
|
+
* :style="inChat ? imageStyle(item) : ''" — 合法
|
|
36
|
+
* :class="[classPrefix, inChat ? classPrefix + '--chatting' : '', getFileTypeClass(...)]" — 合法
|
|
37
|
+
*/
|
|
38
|
+
function checkExpression(expression, attrName) {
|
|
39
|
+
if (!expression) return;
|
|
40
|
+
|
|
41
|
+
const isStyle = attrName === 'style';
|
|
42
|
+
const funcMessageId = isStyle ? 'funcStyleError' : 'funcClassError';
|
|
43
|
+
const tplMessageId = isStyle ? 'tplStyleError' : 'tplClassError';
|
|
44
|
+
|
|
45
|
+
if (expression.type === 'CallExpression') {
|
|
46
|
+
// :style="tool._style(xxx)" / :class="getClass(xxx)"
|
|
47
|
+
context.report({
|
|
48
|
+
node: expression,
|
|
49
|
+
messageId: funcMessageId,
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (expression.type === 'TemplateLiteral') {
|
|
55
|
+
// :style="`color: ${color}`" / :class="`${prefix}-item`"
|
|
56
|
+
context.report({
|
|
57
|
+
node: expression,
|
|
58
|
+
messageId: tplMessageId,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return parserServices.defineTemplateBodyVisitor({
|
|
64
|
+
VAttribute(node) {
|
|
65
|
+
if (!node.key
|
|
66
|
+
|| node.key.type !== 'VDirectiveKey'
|
|
67
|
+
|| node.key?.argument?.type !== 'VIdentifier'
|
|
68
|
+
) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const attrName = node.key?.argument?.name;
|
|
73
|
+
if (attrName !== 'style' && attrName !== 'class') {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (node?.value?.type === 'VExpressionContainer' && node.value.expression) {
|
|
78
|
+
checkExpression(node.value.expression, attrName);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { getParserServices } = require('../utils/parser-services');
|
|
2
2
|
const TEST_REGEX = /\[[^\](]*\.\d+[a-zA-Z]+[^\]]*\]/;
|
|
3
3
|
|
|
4
4
|
|
|
@@ -27,7 +27,12 @@ module.exports = {
|
|
|
27
27
|
const options = context.options[0] || {};
|
|
28
28
|
const regex = options.regexp ?? TEST_REGEX;
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
const parserServices = getParserServices(context);
|
|
31
|
+
if (!parserServices?.defineTemplateBodyVisitor) {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return parserServices.defineTemplateBodyVisitor({
|
|
31
36
|
'VAttribute[key.name=\'class\']'(node) {
|
|
32
37
|
if (!node.value || node.value.type !== 'VLiteral') return;
|
|
33
38
|
|
package/lib/rules/no-js-file.js
CHANGED
|
@@ -50,7 +50,7 @@ module.exports = {
|
|
|
50
50
|
const include = options.include || DEFAULT_INCLUDE;
|
|
51
51
|
const exclude = options.exclude || DEFAULT_EXCLUDE;
|
|
52
52
|
|
|
53
|
-
const filename = context.getFilename();
|
|
53
|
+
const filename = context.filename || context.getFilename();
|
|
54
54
|
const pureFilePath = path.relative(projectRoot, filename);
|
|
55
55
|
|
|
56
56
|
if (!pureFilePath) {
|
|
@@ -10,9 +10,9 @@ module.exports = {
|
|
|
10
10
|
create(context) {
|
|
11
11
|
return {
|
|
12
12
|
Program(node) {
|
|
13
|
-
const sourceCode = context.getSourceCode();
|
|
13
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
14
14
|
const vueFileContent = sourceCode.getText();
|
|
15
|
-
const filename = context.getFilename();
|
|
15
|
+
const filename = context.filename || context.getFilename();
|
|
16
16
|
|
|
17
17
|
if (!filename.endsWith('.vue')) {
|
|
18
18
|
return {};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { getParserServices } = require('../utils/parser-services');
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
meta: {
|
|
3
5
|
type: 'problem',
|
|
@@ -12,16 +14,17 @@ module.exports = {
|
|
|
12
14
|
},
|
|
13
15
|
|
|
14
16
|
create(context) {
|
|
15
|
-
const fileName = context.getFilename();
|
|
17
|
+
const fileName = context.filename || context.getFilename();
|
|
16
18
|
if (!fileName.endsWith('.vue')) {
|
|
17
19
|
return {};
|
|
18
20
|
}
|
|
19
|
-
|
|
21
|
+
const parserServices = getParserServices(context);
|
|
22
|
+
if (!parserServices?.defineTemplateBodyVisitor) {
|
|
20
23
|
return {
|
|
21
24
|
};
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
return
|
|
27
|
+
return parserServices.defineTemplateBodyVisitor({
|
|
25
28
|
VAttribute(node) {
|
|
26
29
|
if (node.value && node.value.type === 'VExpressionContainer' && node.value.expression) {
|
|
27
30
|
const { operator } = node.value.expression;
|
|
@@ -53,7 +53,7 @@ module.exports = {
|
|
|
53
53
|
return {
|
|
54
54
|
ImportDeclaration(node) {
|
|
55
55
|
// 获取当前文件的绝对路径
|
|
56
|
-
const filename = context.getFilename();
|
|
56
|
+
const filename = context.filename || context.getFilename();
|
|
57
57
|
|
|
58
58
|
// 标准化路径(统一使用正斜杠)
|
|
59
59
|
const normalizedPath = filename.replace(/\\/g, '/');
|
|
@@ -22,13 +22,13 @@ module.exports = {
|
|
|
22
22
|
],
|
|
23
23
|
},
|
|
24
24
|
create(context) {
|
|
25
|
-
const sourceCode = context.getSourceCode();
|
|
25
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
26
26
|
const comments = sourceCode.getAllComments();
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const keyword = options.keyword ?? DEFAULT_KEYWORD;
|
|
28
|
+
const options = context.options[0] || {};
|
|
29
|
+
const keyword = options.keyword ?? DEFAULT_KEYWORD;
|
|
31
30
|
|
|
31
|
+
comments.forEach((comment) => {
|
|
32
32
|
if (comment.value.includes(keyword)) {
|
|
33
33
|
context.report({
|
|
34
34
|
node: comment,
|
|
@@ -27,8 +27,9 @@ module.exports = {
|
|
|
27
27
|
create(context) {
|
|
28
28
|
const options = context.options[0] || {};
|
|
29
29
|
const baseDir = options.baseDir || 'src';
|
|
30
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
30
31
|
|
|
31
|
-
const filename = context.getFilename();
|
|
32
|
+
const filename = context.filename || context.getFilename();
|
|
32
33
|
const absolutePath = path.resolve(filename);
|
|
33
34
|
|
|
34
35
|
// 提取当前包名
|
|
@@ -114,7 +115,6 @@ module.exports = {
|
|
|
114
115
|
*/
|
|
115
116
|
const checkAndReport = (node, importPath) => {
|
|
116
117
|
if (importPath && importPath.startsWith(pmdPackageName)) {
|
|
117
|
-
const sourceCode = context.getSourceCode();
|
|
118
118
|
const fixNode = getFixNode(node);
|
|
119
119
|
|
|
120
120
|
context.report({
|
|
@@ -180,7 +180,7 @@ module.exports = {
|
|
|
180
180
|
create(context) {
|
|
181
181
|
// const sourceCode = context.getSourceCode();
|
|
182
182
|
// const cwd = context.getCwd();
|
|
183
|
-
const fileName = context.getFilename();
|
|
183
|
+
const fileName = context.filename || context.getFilename();
|
|
184
184
|
|
|
185
185
|
const dirname = path.dirname(fileName);
|
|
186
186
|
if (!fileName.endsWith('.vue')) {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const { RuleTester } = require('eslint');
|
|
2
|
+
|
|
3
|
+
const rule = require('../rules/img-v-lazy');
|
|
4
|
+
|
|
5
|
+
const FILENAME = 'test.vue';
|
|
6
|
+
|
|
7
|
+
const ruleTester = new RuleTester({
|
|
8
|
+
parser: require.resolve('vue-eslint-parser'),
|
|
9
|
+
parserOptions: {
|
|
10
|
+
ecmaVersion: 'latest',
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
ruleTester.run('img-v-lazy', rule, {
|
|
16
|
+
valid: [
|
|
17
|
+
// 已使用 v-lazy,合法
|
|
18
|
+
{
|
|
19
|
+
code: '<template><img v-lazy="imgUrl" /></template>',
|
|
20
|
+
filename: FILENAME,
|
|
21
|
+
},
|
|
22
|
+
// 静态 src(非绑定),合法
|
|
23
|
+
{
|
|
24
|
+
code: '<template><img src="https://example.com/image.png" /></template>',
|
|
25
|
+
filename: FILENAME,
|
|
26
|
+
},
|
|
27
|
+
// 已同时有 :src 和 v-lazy,合法
|
|
28
|
+
{
|
|
29
|
+
code: '<template><img :src="imgUrl" v-lazy="imgUrl" /></template>',
|
|
30
|
+
filename: FILENAME,
|
|
31
|
+
},
|
|
32
|
+
// 非 img 标签,合法
|
|
33
|
+
{
|
|
34
|
+
code: '<template><div :src="imgUrl"></div></template>',
|
|
35
|
+
filename: FILENAME,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
invalid: [
|
|
39
|
+
// 普通变量绑定,可自动修复
|
|
40
|
+
{
|
|
41
|
+
code: '<template><img :src="imgUrl" /></template>',
|
|
42
|
+
filename: FILENAME,
|
|
43
|
+
errors: [{ messageId: 'useVLazy' }],
|
|
44
|
+
output: '<template><img v-lazy="imgUrl" /></template>',
|
|
45
|
+
},
|
|
46
|
+
// 成员表达式,可自动修复
|
|
47
|
+
{
|
|
48
|
+
code: '<template><img :src="item.imageUrl" /></template>',
|
|
49
|
+
filename: FILENAME,
|
|
50
|
+
errors: [{ messageId: 'useVLazy' }],
|
|
51
|
+
output: '<template><img v-lazy="item.imageUrl" /></template>',
|
|
52
|
+
},
|
|
53
|
+
// 字符串字面量,需手动替换(无 output / fix)
|
|
54
|
+
{
|
|
55
|
+
code: '<template><img :src="\'https://example.com/img.png\'" /></template>',
|
|
56
|
+
filename: FILENAME,
|
|
57
|
+
errors: [{ messageId: 'useVLazyManual' }],
|
|
58
|
+
},
|
|
59
|
+
// 三元表达式,需手动替换(无 output / fix)
|
|
60
|
+
{
|
|
61
|
+
code: '<template><img :src="item.img ? item.img : \'https://example.com/default.png\'" /></template>',
|
|
62
|
+
filename: FILENAME,
|
|
63
|
+
errors: [{ messageId: 'useVLazyManual' }],
|
|
64
|
+
},
|
|
65
|
+
// 逻辑或 + 字符串字面量,用反引号自动修复
|
|
66
|
+
{
|
|
67
|
+
code: '<template><img :src="item.img || \'https://example.com/default.png\'" /></template>',
|
|
68
|
+
filename: FILENAME,
|
|
69
|
+
errors: [{ messageId: 'useVLazy' }],
|
|
70
|
+
output: '<template><img v-lazy=`item.img || \'https://example.com/default.png\'` /></template>',
|
|
71
|
+
},
|
|
72
|
+
// 逻辑或 + 两侧都是变量,正常双引号自动修复
|
|
73
|
+
{
|
|
74
|
+
code: '<template><img :src="item.img || fallbackImg" /></template>',
|
|
75
|
+
filename: FILENAME,
|
|
76
|
+
errors: [{ messageId: 'useVLazy' }],
|
|
77
|
+
output: '<template><img v-lazy="item.img || fallbackImg" /></template>',
|
|
78
|
+
},
|
|
79
|
+
// v-bind:src 写法,可自动修复
|
|
80
|
+
{
|
|
81
|
+
code: '<template><img v-bind:src="imgUrl" /></template>',
|
|
82
|
+
filename: FILENAME,
|
|
83
|
+
errors: [{ messageId: 'useVLazy' }],
|
|
84
|
+
output: '<template><img v-lazy="imgUrl" /></template>',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
});
|
package/lib/test/index.js
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const { RuleTester } = require('eslint');
|
|
2
|
+
|
|
3
|
+
const rule = require('../rules/no-complex-style-class');
|
|
4
|
+
|
|
5
|
+
const FILENAME = 'test.vue';
|
|
6
|
+
|
|
7
|
+
const ruleTester = new RuleTester({
|
|
8
|
+
parser: require.resolve('vue-eslint-parser'),
|
|
9
|
+
parserOptions: {
|
|
10
|
+
ecmaVersion: 'latest',
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
ruleTester.run('no-complex-style-class', rule, {
|
|
16
|
+
valid: [
|
|
17
|
+
// :style 绑定变量 — 合法
|
|
18
|
+
{ code: '<template><div :style="myStyle"></div></template>', filename: FILENAME },
|
|
19
|
+
|
|
20
|
+
// :style 绑定对象(值为变量) — 合法
|
|
21
|
+
{ code: '<template><div :style="{ color: myColor }"></div></template>', filename: FILENAME },
|
|
22
|
+
|
|
23
|
+
// :class 绑定变量 — 合法
|
|
24
|
+
{ code: '<template><div :class="item.className"></div></template>', filename: FILENAME },
|
|
25
|
+
|
|
26
|
+
// :class 绑定字面量数组 — 合法
|
|
27
|
+
{ code: '<template><div :class="[\'a\', \'b\']"></div></template>', filename: FILENAME },
|
|
28
|
+
|
|
29
|
+
// :class 绑定对象 — 合法
|
|
30
|
+
{ code: '<template><div :class="{ active: isActive }"></div></template>', filename: FILENAME },
|
|
31
|
+
|
|
32
|
+
// :style 绑定条件表达式(值为变量) — 合法
|
|
33
|
+
{ code: '<template><div :style="flag ? styleA : styleB"></div></template>', filename: FILENAME },
|
|
34
|
+
|
|
35
|
+
// 静态 style — 合法(非绑定)
|
|
36
|
+
{ code: '<template><div style="color: red"></div></template>', filename: FILENAME },
|
|
37
|
+
|
|
38
|
+
// 静态 class — 合法(非绑定)
|
|
39
|
+
{ code: '<template><div class="foo bar"></div></template>', filename: FILENAME },
|
|
40
|
+
|
|
41
|
+
// 数组元素中的函数调用 — 合法(小程序支持)
|
|
42
|
+
{ code: '<template><div :class="[utils.getClass(classPrefix, dataSize || \'medium\', dataShape, dataBordered), tClassImage]"></div></template>', filename: FILENAME },
|
|
43
|
+
|
|
44
|
+
// 数组中混合使用函数调用和条件表达式 — 合法
|
|
45
|
+
{ code: '<template><div :class="[classPrefix, inChat ? classPrefix + \'--chatting\' : \'\', getFileTypeClass(inChat, files)]"></div></template>', filename: FILENAME },
|
|
46
|
+
|
|
47
|
+
// 条件表达式中的函数调用 — 合法(小程序支持)
|
|
48
|
+
{ code: '<template><div :style="inChat ? imageStyle(item) : \'\'"></div></template>', filename: FILENAME },
|
|
49
|
+
|
|
50
|
+
// 对象属性值中的函数调用 — 合法(小程序支持)
|
|
51
|
+
{ code: '<template><div :style="{ color: getColor() }"></div></template>', filename: FILENAME },
|
|
52
|
+
|
|
53
|
+
// 条件表达式分支中的函数调用 — 合法
|
|
54
|
+
{ code: '<template><div :style="flag ? getStyle() : defaultStyle"></div></template>', filename: FILENAME },
|
|
55
|
+
|
|
56
|
+
// 数组中有多个函数调用 — 合法
|
|
57
|
+
{ code: '<template><div :class="[getA(), getB()]"></div></template>', filename: FILENAME },
|
|
58
|
+
],
|
|
59
|
+
invalid: [
|
|
60
|
+
// :style 中直接使用函数调用(顶层)
|
|
61
|
+
{
|
|
62
|
+
code: '<template><div :style="tool._style(xxx)"></div></template>',
|
|
63
|
+
filename: FILENAME,
|
|
64
|
+
errors: [{ messageId: 'funcStyleError' }],
|
|
65
|
+
},
|
|
66
|
+
// :class 中直接使用函数调用(顶层)
|
|
67
|
+
{
|
|
68
|
+
code: '<template><div :class="getClass(item)"></div></template>',
|
|
69
|
+
filename: FILENAME,
|
|
70
|
+
errors: [{ messageId: 'funcClassError' }],
|
|
71
|
+
},
|
|
72
|
+
// :style 中直接使用模板字符串(顶层)
|
|
73
|
+
{
|
|
74
|
+
code: '<template><div :style="`color: ${color}`"></div></template>',
|
|
75
|
+
filename: FILENAME,
|
|
76
|
+
errors: [{ messageId: 'tplStyleError' }],
|
|
77
|
+
},
|
|
78
|
+
// :class 中直接使用模板字符串(顶层)
|
|
79
|
+
{
|
|
80
|
+
code: '<template><div :class="`${prefix}-item`"></div></template>',
|
|
81
|
+
filename: FILENAME,
|
|
82
|
+
errors: [{ messageId: 'tplClassError' }],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 获取 parserServices,兼容 ESLint v8 和 v9
|
|
3
|
+
*
|
|
4
|
+
* ESLint v9 中 context.parserServices 已被废弃,
|
|
5
|
+
* 应使用 context.sourceCode.parserServices 代替。
|
|
6
|
+
*
|
|
7
|
+
* @param {import('eslint').Rule.RuleContext} context
|
|
8
|
+
* @returns {object} parserServices
|
|
9
|
+
*/
|
|
10
|
+
function getParserServices(context) {
|
|
11
|
+
// ESLint v9+: 优先使用 sourceCode.parserServices
|
|
12
|
+
if (context.sourceCode && context.sourceCode.parserServices) {
|
|
13
|
+
return context.sourceCode.parserServices;
|
|
14
|
+
}
|
|
15
|
+
// ESLint v8: getSourceCode() 方式
|
|
16
|
+
if (typeof context.getSourceCode === 'function') {
|
|
17
|
+
const sourceCode = context.getSourceCode();
|
|
18
|
+
if (sourceCode && sourceCode.parserServices) {
|
|
19
|
+
return sourceCode.parserServices;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// 最终回退到 context.parserServices
|
|
23
|
+
return context.parserServices || {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { getParserServices };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-light",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.23",
|
|
4
|
+
"description": "ESLint 插件",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|
|
7
7
|
"eslint plugin",
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
"CHANGELOG.md"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
+
"@tdesign/uniapp": "^0.7.3",
|
|
29
|
+
"@tdesign/uniapp-chat": "^0.2.1",
|
|
28
30
|
"minimatch": "^10.0.1",
|
|
29
31
|
"requireindex": "^1.2.0",
|
|
30
32
|
"t-comm": "^3.0.22"
|
|
@@ -48,6 +50,10 @@
|
|
|
48
50
|
"engines": {
|
|
49
51
|
"node": "12.x || 14.x || >= 16"
|
|
50
52
|
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public",
|
|
55
|
+
"registry": "https://registry.npmjs.org/"
|
|
56
|
+
},
|
|
51
57
|
"scripts": {
|
|
52
58
|
"build": "rm -rf lib && node script/build",
|
|
53
59
|
"bump": "node ../../script/monorepo/version-simple $PWD",
|