eslint-plugin-crisp 1.5.13 → 1.6.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/.github/workflows/build.yml +6 -0
- package/README.md +2 -1
- package/index.js +2 -0
- package/package.json +9 -1
- package/recommended-vue.js +1 -0
- package/rules/align-comments.js +6 -3
- package/rules/constructor-variables.js +17 -2
- package/rules/header-check.js +8 -2
- package/rules/header-comments-check.js +14 -3
- package/rules/import-group-comment.js +13 -4
- package/rules/jsdoc-enforce-access.js +16 -10
- package/rules/no-extra-line-within-function.js +8 -5
- package/rules/no-short-parameters.js +1 -1
- package/rules/no-snake-case.js +0 -24
- package/rules/no-space-in-optional-arguments.js +5 -2
- package/rules/one-space-after-operator.js +59 -14
- package/rules/vue-header-check.js +4 -3
- package/rules/vue-no-regex-data.js +1 -2
- package/rules/vue-pinia-action-single-parameter.js +158 -0
- package/rules/vue-props-declaration-line-break.js +8 -3
- package/rules/vue-watch-handler.js +34 -1
- package/tests/fixtures/vue-header-check/missing-script-style/TestComponent.vue +19 -0
- package/tests/fixtures/vue-header-check/missing-template/TestComponent.vue +3 -0
- package/tests/fixtures/vue-header-check/valid/TestComponent.vue +27 -0
- package/tests/rule-tester.js +78 -0
- package/tests/rules/align-comments.test.js +25 -0
- package/tests/rules/align-consecutive-class-assignements.test.js +24 -0
- package/tests/rules/align-one-var.test.js +31 -0
- package/tests/rules/align-requires.test.js +27 -0
- package/tests/rules/arrow-block-newline.test.js +23 -0
- package/tests/rules/consecutive-line-comments.test.js +27 -0
- package/tests/rules/const.test.js +52 -0
- package/tests/rules/constructor-variables.test.js +52 -0
- package/tests/rules/enforce-optional.test.js +24 -0
- package/tests/rules/header-check.test.js +69 -0
- package/tests/rules/header-comments-check.test.js +90 -0
- package/tests/rules/import-group-comment.test.js +88 -0
- package/tests/rules/import-group-order.test.js +58 -0
- package/tests/rules/jsdoc-align-params.test.js +32 -0
- package/tests/rules/jsdoc-check-indentation.test.js +45 -0
- package/tests/rules/jsdoc-check-optional-params.test.js +40 -0
- package/tests/rules/jsdoc-description-length.test.js +60 -0
- package/tests/rules/jsdoc-description-multiline.test.js +34 -0
- package/tests/rules/jsdoc-enforce-access.test.js +66 -0
- package/tests/rules/jsdoc-enforce-classdesc.test.js +39 -0
- package/tests/rules/jsdoc-no-param-return.test.js +47 -0
- package/tests/rules/jsdoc-require-description-uppercase.test.js +52 -0
- package/tests/rules/methods-naming.test.js +70 -0
- package/tests/rules/methods-ordering.test.js +75 -0
- package/tests/rules/multiline-comment-end-backslash.test.js +52 -0
- package/tests/rules/newline-after-switch-case.test.js +75 -0
- package/tests/rules/no-async.test.js +57 -0
- package/tests/rules/no-extra-line-within-function.test.js +87 -0
- package/tests/rules/no-inline-function-in-ternary.test.js +59 -0
- package/tests/rules/no-short-parameters.test.js +62 -0
- package/tests/rules/no-snake-case.test.js +81 -0
- package/tests/rules/no-space-in-optional-arguments.test.js +40 -0
- package/tests/rules/no-useless-template-literals.test.js +36 -0
- package/tests/rules/no-var-in-blocks.test.js +40 -0
- package/tests/rules/one-space-after-operator.test.js +69 -0
- package/tests/rules/regex-in-constructor.test.js +40 -0
- package/tests/rules/ternary-parenthesis.test.js +34 -0
- package/tests/rules/two-lines-between-class-members.test.js +62 -0
- package/tests/rules/variable-names.test.js +60 -0
- package/tests/rules/vue-attribute-comma.test.js +43 -0
- package/tests/rules/vue-attribute-linebreak.test.js +54 -0
- package/tests/rules/vue-class-attribute-last.test.js +59 -0
- package/tests/rules/vue-class-shorthand-max-len.test.js +67 -0
- package/tests/rules/vue-computed-order.test.js +27 -0
- package/tests/rules/vue-data-comment.test.js +61 -0
- package/tests/rules/vue-define-component.test.js +45 -0
- package/tests/rules/vue-emits-order.test.js +27 -0
- package/tests/rules/vue-header-check.test.js +49 -0
- package/tests/rules/vue-html-indent.test.js +77 -0
- package/tests/rules/vue-html-quotes.test.js +71 -0
- package/tests/rules/vue-methods-separator.test.js +55 -0
- package/tests/rules/vue-name-prop.test.js +59 -0
- package/tests/rules/vue-no-component-class-shorthand.test.js +49 -0
- package/tests/rules/vue-no-mixed-class.test.js +56 -0
- package/tests/rules/vue-no-name-option.test.js +44 -0
- package/tests/rules/vue-no-regex-data.test.js +39 -0
- package/tests/rules/vue-pinia-action-single-parameter.test.js +234 -0
- package/tests/rules/vue-props-declaration-line-break.test.js +68 -0
- package/tests/rules/vue-props-declaration-multiline.test.js +40 -0
- package/tests/rules/vue-props-declaration-order.test.js +27 -0
- package/tests/rules/vue-ref-case.test.js +40 -0
- package/tests/rules/vue-watch-handler.test.js +89 -0
- package/tests/rules-coverage.test.js +19 -0
|
@@ -26,5 +26,11 @@ jobs:
|
|
|
26
26
|
- name: Verify versions
|
|
27
27
|
run: node --version && npm --version && node -p process.versions.v8
|
|
28
28
|
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: npm ci
|
|
31
|
+
|
|
32
|
+
- name: Run tests
|
|
33
|
+
run: npm test
|
|
34
|
+
|
|
29
35
|
- name: Release package
|
|
30
36
|
run: npm publish --ignore-scripts --provenance
|
package/README.md
CHANGED
|
@@ -133,12 +133,13 @@ Each item has emojis denoting:
|
|
|
133
133
|
| [crisp/multiline-comment-end-backslash](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/multiline-comment-end-backslash.js) | Enforces multiline comments to end with a backslash | 🟠 | 🟢 | 🟣 |
|
|
134
134
|
| [crisp/newline-after-switch-case](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/newline-after-switch-case.js) | Requires newline between switch cases | 🟠 | 🟢 | 🟣 |
|
|
135
135
|
| [crisp/no-async](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/no-async.js) | Disallows the use of `async/syntax`, in favor of `Promise` | 🟠 | 🟢 |
|
|
136
|
-
| [crisp/no-short-parameters](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/no-short-parameters.js) |
|
|
136
|
+
| [crisp/no-short-parameters](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/no-short-parameters.js) | Disallows one-character parameter names | 🟠 | 🟢 | 🟣 |
|
|
137
137
|
| [crisp/no-space-in-optional-arguments](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/no-space-in-optional-arguments.js) | Disallows space before or after `=` in optional parameters | 🟠 |
|
|
138
138
|
| [crisp/no-snake-case](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/no-snake-case.js) | Disallows snake_case | | 🟢 | 🟣 |
|
|
139
139
|
| [crisp/no-useless-template-literals](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/no-useless-template-literals.js) | Disallows unnecessary use of template literals | | 🟢 | 🟣 |
|
|
140
140
|
| [crisp/no-var-in-blocks](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/no-var-in-blocks.js) | Disallows `var` declarations inside function, method, or class block | 🟠 | 🟢 | 🟣 |
|
|
141
141
|
| [crisp/one-space-after-operator](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/one-space-after-operator.js) | Enforces at least one space before and one space after `=` and `:` operators | 🟠 | 🟢 | 🟣 |
|
|
142
|
+
| [crisp/vue-pinia-action-single-parameter](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/vue-pinia-action-single-parameter.js) | Requires Pinia actions to accept at most one payload argument | | 🟢 | |
|
|
142
143
|
| [crisp/regex-in-constructor](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/regex-in-constructor.js) | Ensures regular expressions are defined in the class constructor | 🟠 | 🟢 | 🟣 |
|
|
143
144
|
| [crisp/ternary-parenthesis](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/ternary-parenthesis.js) | Enforces parentheses around conditions with operators in ternary expressions | 🟠 | 🟢 | 🟣 |
|
|
144
145
|
| [crisp/two-lines-between-class-members](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/two-lines-between-class-members.js) | Requires exactly two line breaks between class methods | 🟠 |
|
package/index.js
CHANGED
|
@@ -58,6 +58,7 @@ import ruleVueNoComponentClassShorthand from "./rules/vue-no-component-class-sho
|
|
|
58
58
|
import ruleVueNoMixedClass from "./rules/vue-no-mixed-class.js";
|
|
59
59
|
import ruleVueNoNameOption from "./rules/vue-no-name-option.js";
|
|
60
60
|
import ruleVueNoRegexData from "./rules/vue-no-regex-data.js";
|
|
61
|
+
import ruleVuePiniaActionSingleParameter from "./rules/vue-pinia-action-single-parameter.js";
|
|
61
62
|
import ruleVuePropsDeclarationLineBreak from "./rules/vue-props-declaration-line-break.js";
|
|
62
63
|
import ruleVuePropsDeclarationMultiline from "./rules/vue-props-declaration-multiline.js";
|
|
63
64
|
import ruleVuePropsDeclarationOrder from "./rules/vue-props-declaration-order.js";
|
|
@@ -134,6 +135,7 @@ const plugin = {
|
|
|
134
135
|
"vue-no-mixed-class": ruleVueNoMixedClass,
|
|
135
136
|
"vue-no-name-option": ruleVueNoNameOption,
|
|
136
137
|
"vue-no-regex-data": ruleVueNoRegexData,
|
|
138
|
+
"vue-pinia-action-single-parameter": ruleVuePiniaActionSingleParameter,
|
|
137
139
|
"vue-props-declaration-line-break": ruleVuePropsDeclarationLineBreak,
|
|
138
140
|
"vue-props-declaration-multiline": ruleVuePropsDeclarationMultiline,
|
|
139
141
|
"vue-props-declaration-order": ruleVuePropsDeclarationOrder,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-crisp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Custom ESLint Rules for Crisp",
|
|
5
5
|
"author": "Crisp IM SAS",
|
|
6
6
|
"main": "index.js",
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/crisp-oss/eslint-plugin-crisp/issues"
|
|
15
15
|
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test"
|
|
18
|
+
},
|
|
16
19
|
"dependencies": {
|
|
17
20
|
"@eslint/js": "9.39.5",
|
|
18
21
|
"doctrine": "3.0.0",
|
|
@@ -21,6 +24,11 @@
|
|
|
21
24
|
"typescript": "5.9.3",
|
|
22
25
|
"typescript-eslint": "8.72.0"
|
|
23
26
|
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"eslint": "9.39.5",
|
|
29
|
+
"vue-eslint-parser": "10.4.1",
|
|
30
|
+
"vue-eslint-parser-template-tokenizer-pug": "1.0.0"
|
|
31
|
+
},
|
|
24
32
|
"peerDependencies": {
|
|
25
33
|
"eslint": "^9.39.5",
|
|
26
34
|
"eslint-plugin-vue": "^10.11.1",
|
package/recommended-vue.js
CHANGED
|
@@ -393,6 +393,7 @@ export default function configRecommendedVue(pluginCrisp) {
|
|
|
393
393
|
"crisp/vue-no-mixed-class": "error",
|
|
394
394
|
"crisp/vue-no-name-option": "error",
|
|
395
395
|
"crisp/vue-no-regex-data": "error",
|
|
396
|
+
"crisp/vue-pinia-action-single-parameter": "error",
|
|
396
397
|
"crisp/vue-props-declaration-line-break": "error",
|
|
397
398
|
"crisp/vue-props-declaration-multiline": "error",
|
|
398
399
|
"crisp/vue-props-declaration-order": "error",
|
package/rules/align-comments.js
CHANGED
|
@@ -52,9 +52,12 @@ export default {
|
|
|
52
52
|
|
|
53
53
|
fix(fixer) {
|
|
54
54
|
const indent = " ".repeat(nodeIndent);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
const lineStart = sourceCode.getIndexFromLoc({
|
|
56
|
+
line: comment.loc.start.line,
|
|
57
|
+
column: 0
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return fixer.replaceTextRange([lineStart, comment.range[0]], indent);
|
|
58
61
|
},
|
|
59
62
|
});
|
|
60
63
|
}
|
|
@@ -25,7 +25,7 @@ export default {
|
|
|
25
25
|
|
|
26
26
|
create(context) {
|
|
27
27
|
const options = context.options[0] || {};
|
|
28
|
-
const filename = context.getFilename();
|
|
28
|
+
const filename = context.filename || context.getFilename();
|
|
29
29
|
const filenameExceptions = options.filenameExceptions || [];
|
|
30
30
|
const variableExceptions = options.variableExceptions || [];
|
|
31
31
|
|
|
@@ -34,7 +34,22 @@ export default {
|
|
|
34
34
|
return {
|
|
35
35
|
"MethodDefinition[kind='constructor'] > FunctionExpression > BlockStatement > ExpressionStatement > AssignmentExpression": function(node) {
|
|
36
36
|
if(node.left.type === "MemberExpression" && node.left.object.type === "ThisExpression") {
|
|
37
|
-
|
|
37
|
+
let varName = null;
|
|
38
|
+
|
|
39
|
+
if (!node.left.computed && node.left.property.type === "Identifier") {
|
|
40
|
+
varName = node.left.property.name;
|
|
41
|
+
} else if (
|
|
42
|
+
node.left.computed &&
|
|
43
|
+
node.left.property.type === "Literal" &&
|
|
44
|
+
typeof node.left.property.value === "string"
|
|
45
|
+
) {
|
|
46
|
+
varName = node.left.property.value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!varName) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
if (!varName.startsWith("_") && !isFilenameException && !variableExceptions.includes(varName)) {
|
|
39
54
|
context.report({
|
|
40
55
|
node,
|
package/rules/header-check.js
CHANGED
|
@@ -55,9 +55,15 @@ export default {
|
|
|
55
55
|
|
|
56
56
|
if (headerStart && headerEnd) {
|
|
57
57
|
// Construct the final headerFormat RegExps
|
|
58
|
-
const headerFormats = basePatterns.
|
|
58
|
+
const headerFormats = basePatterns.flatMap(pattern => {
|
|
59
|
+
try {
|
|
60
|
+
return [new RegExp("^" + headerStart + pattern + headerEnd)];
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
});
|
|
59
65
|
|
|
60
|
-
if (headerFormats.some(format => format
|
|
66
|
+
if (headerFormats.some(format => format.test(fileContent)) === false) {
|
|
61
67
|
context.report({
|
|
62
68
|
node,
|
|
63
69
|
message: "File must start with the proper header.",
|
|
@@ -113,7 +113,7 @@ export default {
|
|
|
113
113
|
},
|
|
114
114
|
|
|
115
115
|
create(context) {
|
|
116
|
-
const filename = context.getFilename();
|
|
116
|
+
const filename = context.filename || context.getFilename();
|
|
117
117
|
|
|
118
118
|
if (!filename.endsWith(".js") && !filename.endsWith(".ts")) {
|
|
119
119
|
return {};
|
|
@@ -223,7 +223,10 @@ export default {
|
|
|
223
223
|
|
|
224
224
|
case "ExportDefaultDeclaration": {
|
|
225
225
|
// Only enforce the 'EXPORTS' comment block if it's the last statement in the file
|
|
226
|
-
if (
|
|
226
|
+
if (
|
|
227
|
+
startNode === sourceCode.ast.body.slice(-1)[0] &&
|
|
228
|
+
(!comment || `/*${comment.value.trim()}*/` !== COMMENT_HEADER_FORMAT("EXPORTS"))
|
|
229
|
+
) {
|
|
227
230
|
context.report({
|
|
228
231
|
node: startNode,
|
|
229
232
|
message: "Export group must be preceded by a 'EXPORTS' comment block",
|
|
@@ -237,9 +240,17 @@ export default {
|
|
|
237
240
|
|
|
238
241
|
return {
|
|
239
242
|
":statement": (node) => {
|
|
243
|
+
if (node.parent.type !== "Program") {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
240
247
|
// If the type of node has changed since last time, or if we have a \
|
|
241
248
|
// regex declaration and the previous declaration wasn't one
|
|
242
|
-
if (
|
|
249
|
+
if (
|
|
250
|
+
!lastNode ||
|
|
251
|
+
node.type !== lastNode.type ||
|
|
252
|
+
isRegex(node) !== isRegex(lastNode)
|
|
253
|
+
) {
|
|
243
254
|
// Check the group starting with the last node
|
|
244
255
|
if (groupStart) {
|
|
245
256
|
checkGroup(lastNode.type, groupStart);
|
|
@@ -29,9 +29,14 @@ function isIgnoreComment(comment) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function getDirectoryName(filePath) {
|
|
32
|
-
|
|
32
|
+
if (!filePath || filePath.startsWith("<")) {
|
|
33
|
+
return "MAIN";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const pathParts = filePath.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
37
|
+
const directoryName = pathParts.at(-2);
|
|
33
38
|
|
|
34
|
-
return
|
|
39
|
+
return directoryName ? directoryName.toUpperCase() : "MAIN";
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
function generateExpectedComment(group) {
|
|
@@ -70,8 +75,12 @@ export default {
|
|
|
70
75
|
const customGroups = context.options[0] || {}; // Get custom groups from options
|
|
71
76
|
|
|
72
77
|
// Pre-compile custom regexes once instead of on every import
|
|
73
|
-
const compiledCustomGroups = Object.entries(customGroups).
|
|
74
|
-
|
|
78
|
+
const compiledCustomGroups = Object.entries(customGroups).flatMap(([regexStr, group]) => {
|
|
79
|
+
try {
|
|
80
|
+
return [[new RegExp(regexStr), group]];
|
|
81
|
+
} catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
75
84
|
});
|
|
76
85
|
|
|
77
86
|
let currentGroupComment = null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Pre-compiled regex for access modifier check
|
|
2
|
-
const ACCESS_MODIFIER_PATTERN = /@(public|protected|private|internal)/;
|
|
2
|
+
const ACCESS_MODIFIER_PATTERN = /@(public|protected|private|internal)(?=\s|\*|$)/;
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
meta: {
|
|
@@ -16,19 +16,25 @@ export default {
|
|
|
16
16
|
|
|
17
17
|
function checkNodeForJSDoc(node) {
|
|
18
18
|
const comments = sourceCode.getCommentsBefore(node);
|
|
19
|
+
let jsdocComment = null;
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
for (let i = comments.length - 1; i >= 0; i--) {
|
|
22
|
+
if (
|
|
23
|
+
comments[i].type === "Block" &&
|
|
24
|
+
comments[i].value.startsWith("*")
|
|
25
|
+
) {
|
|
26
|
+
jsdocComment = comments[i];
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
if (!ACCESS_MODIFIER_PATTERN.test(lastComment.value)) {
|
|
25
|
-
context.report({
|
|
26
|
-
node,
|
|
27
|
-
message: "JSDoc comment should contain @public, @protected, @private or @internal",
|
|
28
|
-
});
|
|
29
|
-
}
|
|
28
|
+
break;
|
|
30
29
|
}
|
|
31
30
|
}
|
|
31
|
+
|
|
32
|
+
if (jsdocComment && !ACCESS_MODIFIER_PATTERN.test(jsdocComment.value)) {
|
|
33
|
+
context.report({
|
|
34
|
+
node,
|
|
35
|
+
message: "JSDoc comment should contain @public, @protected, @private or @internal",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
return {
|
|
@@ -37,11 +37,14 @@ export default {
|
|
|
37
37
|
node: currentNode,
|
|
38
38
|
message: "Expected at most one line break between declarations within a function.",
|
|
39
39
|
fix(fixer) {
|
|
40
|
-
const range = [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
const range = [previousNode.range[1], currentNode.range[0]];
|
|
41
|
+
const textBetween = sourceCode.text.slice(...range);
|
|
42
|
+
const fixedText = textBetween.replace(
|
|
43
|
+
/\n(?:[ \t]*\n){2,}/g,
|
|
44
|
+
"\n\n"
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return fixer.replaceTextRange(range, fixedText);
|
|
45
48
|
}
|
|
46
49
|
});
|
|
47
50
|
}
|
package/rules/no-snake-case.js
CHANGED
|
@@ -92,18 +92,6 @@ export default {
|
|
|
92
92
|
reportSnakeCase(node, name, "Method");
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
// Check method parameters
|
|
97
|
-
if (node.value && node.value.params) {
|
|
98
|
-
node.value.params.forEach((param) => {
|
|
99
|
-
if (param.type === "Identifier" && param.name) {
|
|
100
|
-
const name = param.name;
|
|
101
|
-
if (isSnakeCase(name)) {
|
|
102
|
-
reportSnakeCase(param, name, "Parameter");
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
95
|
},
|
|
108
96
|
|
|
109
97
|
// Check function expressions
|
|
@@ -142,18 +130,6 @@ export default {
|
|
|
142
130
|
reportSnakeCase(node, name, "Method");
|
|
143
131
|
}
|
|
144
132
|
}
|
|
145
|
-
|
|
146
|
-
// Check method parameters
|
|
147
|
-
if (node.value && node.value.params) {
|
|
148
|
-
node.value.params.forEach((param) => {
|
|
149
|
-
if (param.type === "Identifier" && param.name) {
|
|
150
|
-
const name = param.name;
|
|
151
|
-
if (isSnakeCase(name)) {
|
|
152
|
-
reportSnakeCase(param, name, "Parameter");
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
133
|
}
|
|
158
134
|
}
|
|
159
135
|
};
|
|
@@ -4,10 +4,13 @@ export default {
|
|
|
4
4
|
"FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, MethodDefinition": function(node) {
|
|
5
5
|
(node.params || []).forEach(param => {
|
|
6
6
|
if (param.type === "AssignmentPattern") {
|
|
7
|
-
const sourceCode = context.getSourceCode();
|
|
7
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
8
8
|
const operatorToken = sourceCode.getFirstTokenBetween(param.left, param.right, token => token.value === "=");
|
|
9
9
|
|
|
10
|
-
if (
|
|
10
|
+
if (
|
|
11
|
+
sourceCode.getTokenBefore(operatorToken).range[1] !== operatorToken.range[0] ||
|
|
12
|
+
sourceCode.getTokenAfter(operatorToken).range[0] !== operatorToken.range[1]
|
|
13
|
+
) {
|
|
11
14
|
context.report({
|
|
12
15
|
node,
|
|
13
16
|
message: "There should be no space before or after = in optional parameters"
|
|
@@ -27,17 +27,47 @@ export default {
|
|
|
27
27
|
const config = context.options[0] || {};
|
|
28
28
|
const checkColon = config.checkColon !== false;
|
|
29
29
|
|
|
30
|
-
const checkSpacing = (
|
|
31
|
-
if (
|
|
30
|
+
const checkSpacing = (operatorToken) => {
|
|
31
|
+
if (!operatorToken) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const tokenBefore = sourceCode.getTokenBefore(operatorToken);
|
|
36
|
+
const tokenAfter = sourceCode.getTokenAfter(operatorToken);
|
|
37
|
+
const beforeText = sourceCode.text.slice(
|
|
38
|
+
tokenBefore.range[1],
|
|
39
|
+
operatorToken.range[0]
|
|
40
|
+
);
|
|
41
|
+
const afterText = sourceCode.text.slice(
|
|
42
|
+
operatorToken.range[1],
|
|
43
|
+
tokenAfter.range[0]
|
|
44
|
+
);
|
|
45
|
+
const hasSpaceBefore = /[ \t]$/.test(beforeText);
|
|
46
|
+
const hasOneSpaceAfter = afterText === " ";
|
|
47
|
+
|
|
48
|
+
if (!hasSpaceBefore || !hasOneSpaceAfter) {
|
|
32
49
|
context.report({
|
|
33
50
|
node: operatorToken,
|
|
34
|
-
message: `There should be at least one space before and exactly one space after '${
|
|
51
|
+
message: `There should be at least one space before and exactly one space after '${operatorToken.value}'`,
|
|
35
52
|
|
|
36
53
|
fix(fixer) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
54
|
+
const fixes = [];
|
|
55
|
+
|
|
56
|
+
if (!hasSpaceBefore && /^[ \t]*$/.test(beforeText)) {
|
|
57
|
+
fixes.push(fixer.replaceTextRange(
|
|
58
|
+
[tokenBefore.range[1], operatorToken.range[0]],
|
|
59
|
+
" "
|
|
60
|
+
));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!hasOneSpaceAfter && /^[ \t]*$/.test(afterText)) {
|
|
64
|
+
fixes.push(fixer.replaceTextRange(
|
|
65
|
+
[operatorToken.range[1], tokenAfter.range[0]],
|
|
66
|
+
" "
|
|
67
|
+
));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return fixes.length ? fixes : null;
|
|
41
71
|
}
|
|
42
72
|
});
|
|
43
73
|
}
|
|
@@ -45,20 +75,35 @@ export default {
|
|
|
45
75
|
|
|
46
76
|
return {
|
|
47
77
|
AssignmentExpression(node) {
|
|
48
|
-
const operatorToken = sourceCode.
|
|
49
|
-
|
|
78
|
+
const operatorToken = sourceCode.getFirstTokenBetween(
|
|
79
|
+
node.left,
|
|
80
|
+
node.right,
|
|
81
|
+
token => token.value === node.operator
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
checkSpacing(operatorToken);
|
|
50
85
|
},
|
|
51
86
|
VariableDeclarator(node) {
|
|
52
87
|
if (node.init) {
|
|
53
|
-
const operatorToken = sourceCode.
|
|
54
|
-
|
|
88
|
+
const operatorToken = sourceCode.getFirstTokenBetween(
|
|
89
|
+
node.id,
|
|
90
|
+
node.init,
|
|
91
|
+
token => token.value === "="
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
checkSpacing(operatorToken);
|
|
55
95
|
}
|
|
56
96
|
},
|
|
57
97
|
|
|
58
98
|
Property(node) {
|
|
59
|
-
if (checkColon) {
|
|
60
|
-
const operatorToken = sourceCode.
|
|
61
|
-
|
|
99
|
+
if (checkColon && !node.method && !node.shorthand) {
|
|
100
|
+
const operatorToken = sourceCode.getFirstTokenBetween(
|
|
101
|
+
node.key,
|
|
102
|
+
node.value,
|
|
103
|
+
token => token.value === ":"
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
checkSpacing(operatorToken);
|
|
62
107
|
}
|
|
63
108
|
},
|
|
64
109
|
};
|
|
@@ -15,13 +15,14 @@ export default {
|
|
|
15
15
|
create(context) {
|
|
16
16
|
return {
|
|
17
17
|
Program(node) {
|
|
18
|
-
const fileName = context.getFilename();
|
|
19
|
-
const fileContent = fs.readFileSync(path.resolve(fileName), "utf8");
|
|
18
|
+
const fileName = context.physicalFilename || context.filename || context.getFilename();
|
|
20
19
|
|
|
21
20
|
// Determine the file extension
|
|
22
21
|
const fileExtension = path.extname(fileName);
|
|
23
22
|
|
|
24
23
|
if (fileExtension === ".vue") {
|
|
24
|
+
const fileContent = fs.readFileSync(path.resolve(fileName), "utf8");
|
|
25
|
+
|
|
25
26
|
// Define the headers
|
|
26
27
|
const headers = ["TEMPLATE", "SCRIPT", "STYLE"];
|
|
27
28
|
|
|
@@ -31,7 +32,7 @@ export default {
|
|
|
31
32
|
// Check each tag individually
|
|
32
33
|
vueTags.forEach((tag, index) => {
|
|
33
34
|
const tagRegExp = new RegExp(`<${tag}`);
|
|
34
|
-
const headerRegExp = new RegExp(
|
|
35
|
+
const headerRegExp = new RegExp(`(?:^|\\n\\n)<!--\\s*\\*{70,73}\\s*\\n\\s{5}${headers[index]}\\s*\\n\\s*\\*{70,73}\\s*-->\\s*\\n\\n<${tag}`);
|
|
35
36
|
|
|
36
37
|
// If the tag exists without the corresponding header, report an error
|
|
37
38
|
if (tagRegExp.test(fileContent) && !headerRegExp.test(fileContent)) {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const MESSAGE_ID = "singlePayloadArgument";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "problem",
|
|
6
|
+
docs: {
|
|
7
|
+
description: "Require Pinia actions to accept at most one payload argument",
|
|
8
|
+
category: "Possible Errors",
|
|
9
|
+
recommended: true
|
|
10
|
+
},
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: {
|
|
13
|
+
[MESSAGE_ID]: "Pinia actions must accept at most one payload argument. Wrap multiple values in an object."
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
create(context) {
|
|
18
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
19
|
+
const defineStoreImports = collectDefineStoreImports(sourceCode.ast);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
CallExpression(node) {
|
|
23
|
+
if (!isImportedDefineStoreCall(node, sourceCode, defineStoreImports)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const options = node.arguments[1];
|
|
28
|
+
|
|
29
|
+
if (!options || options.type !== "ObjectExpression") {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const actions = findObjectProperty(options, "actions");
|
|
34
|
+
|
|
35
|
+
if (!actions || actions.value.type !== "ObjectExpression") {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const property of actions.value.properties) {
|
|
40
|
+
const action = getActionFunction(property);
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
action &&
|
|
44
|
+
(
|
|
45
|
+
action.params.length > 1 ||
|
|
46
|
+
action.params.some((parameter) => parameter.type === "RestElement")
|
|
47
|
+
)
|
|
48
|
+
) {
|
|
49
|
+
context.report({
|
|
50
|
+
node: action,
|
|
51
|
+
messageId: MESSAGE_ID
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Collects local bindings for named defineStore imports from Pinia
|
|
61
|
+
function collectDefineStoreImports(program) {
|
|
62
|
+
const imports = new Map();
|
|
63
|
+
|
|
64
|
+
for (const node of program.body) {
|
|
65
|
+
if (node.type !== "ImportDeclaration" || node.source.value !== "pinia") {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const specifier of node.specifiers) {
|
|
70
|
+
if (
|
|
71
|
+
specifier.type === "ImportSpecifier" &&
|
|
72
|
+
getImportedName(specifier) === "defineStore"
|
|
73
|
+
) {
|
|
74
|
+
imports.set(specifier.local.name, specifier);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return imports;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Returns the imported name for JavaScript and TypeScript ESTree nodes
|
|
83
|
+
function getImportedName(specifier) {
|
|
84
|
+
return specifier.imported.name || specifier.imported.value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Checks that a call resolves to the collected Pinia import, rather than a shadow
|
|
88
|
+
function isImportedDefineStoreCall(node, sourceCode, defineStoreImports) {
|
|
89
|
+
if (
|
|
90
|
+
node.callee.type !== "Identifier" ||
|
|
91
|
+
!defineStoreImports.has(node.callee.name)
|
|
92
|
+
) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const expectedSpecifier = defineStoreImports.get(node.callee.name);
|
|
97
|
+
let scope = sourceCode.getScope(node);
|
|
98
|
+
|
|
99
|
+
while (scope) {
|
|
100
|
+
const variable = scope.variables.find(({ name }) => name === node.callee.name);
|
|
101
|
+
|
|
102
|
+
if (variable) {
|
|
103
|
+
return variable.defs.some((definition) => {
|
|
104
|
+
return (
|
|
105
|
+
definition.type === "ImportBinding" &&
|
|
106
|
+
definition.node === expectedSpecifier
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
scope = scope.upper;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Finds a direct, statically named property in an object expression
|
|
118
|
+
function findObjectProperty(object, name) {
|
|
119
|
+
return object.properties.find((property) => {
|
|
120
|
+
return (
|
|
121
|
+
property.type === "Property" &&
|
|
122
|
+
property.kind === "init" &&
|
|
123
|
+
getStaticPropertyName(property) === name
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Returns a property's static name, including quoted property names
|
|
129
|
+
function getStaticPropertyName(property) {
|
|
130
|
+
if (!property.computed && property.key.type === "Identifier") {
|
|
131
|
+
return property.key.name;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (property.key.type === "Literal") {
|
|
135
|
+
return property.key.value;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Returns a directly declared action function
|
|
142
|
+
function getActionFunction(property) {
|
|
143
|
+
if (
|
|
144
|
+
property.type !== "Property" ||
|
|
145
|
+
property.kind !== "init"
|
|
146
|
+
) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
property.value.type === "FunctionExpression" ||
|
|
152
|
+
property.value.type === "ArrowFunctionExpression"
|
|
153
|
+
) {
|
|
154
|
+
return property.value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return null;
|
|
158
|
+
}
|