eslint-plugin-crisp 1.5.13 → 1.6.1

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.
Files changed (88) hide show
  1. package/.github/workflows/build.yml +6 -0
  2. package/README.md +2 -1
  3. package/index.js +2 -0
  4. package/package.json +9 -1
  5. package/recommended-vue.js +1 -0
  6. package/rules/align-comments.js +6 -3
  7. package/rules/constructor-variables.js +17 -2
  8. package/rules/header-check.js +8 -2
  9. package/rules/header-comments-check.js +10 -13
  10. package/rules/import-group-comment.js +13 -4
  11. package/rules/jsdoc-enforce-access.js +16 -10
  12. package/rules/no-extra-line-within-function.js +8 -5
  13. package/rules/no-short-parameters.js +1 -1
  14. package/rules/no-snake-case.js +0 -24
  15. package/rules/no-space-in-optional-arguments.js +5 -2
  16. package/rules/one-space-after-operator.js +59 -14
  17. package/rules/vue-header-check.js +4 -3
  18. package/rules/vue-no-regex-data.js +1 -2
  19. package/rules/vue-pinia-action-single-parameter.js +158 -0
  20. package/rules/vue-props-declaration-line-break.js +8 -3
  21. package/rules/vue-watch-handler.js +34 -1
  22. package/tests/fixtures/vue-header-check/missing-script-style/TestComponent.vue +19 -0
  23. package/tests/fixtures/vue-header-check/missing-template/TestComponent.vue +3 -0
  24. package/tests/fixtures/vue-header-check/valid/TestComponent.vue +27 -0
  25. package/tests/rule-tester.js +78 -0
  26. package/tests/rules/align-comments.test.js +25 -0
  27. package/tests/rules/align-consecutive-class-assignements.test.js +24 -0
  28. package/tests/rules/align-one-var.test.js +31 -0
  29. package/tests/rules/align-requires.test.js +27 -0
  30. package/tests/rules/arrow-block-newline.test.js +23 -0
  31. package/tests/rules/consecutive-line-comments.test.js +27 -0
  32. package/tests/rules/const.test.js +52 -0
  33. package/tests/rules/constructor-variables.test.js +52 -0
  34. package/tests/rules/enforce-optional.test.js +24 -0
  35. package/tests/rules/header-check.test.js +69 -0
  36. package/tests/rules/header-comments-check.test.js +89 -0
  37. package/tests/rules/import-group-comment.test.js +88 -0
  38. package/tests/rules/import-group-order.test.js +58 -0
  39. package/tests/rules/jsdoc-align-params.test.js +32 -0
  40. package/tests/rules/jsdoc-check-indentation.test.js +45 -0
  41. package/tests/rules/jsdoc-check-optional-params.test.js +40 -0
  42. package/tests/rules/jsdoc-description-length.test.js +60 -0
  43. package/tests/rules/jsdoc-description-multiline.test.js +34 -0
  44. package/tests/rules/jsdoc-enforce-access.test.js +66 -0
  45. package/tests/rules/jsdoc-enforce-classdesc.test.js +39 -0
  46. package/tests/rules/jsdoc-no-param-return.test.js +47 -0
  47. package/tests/rules/jsdoc-require-description-uppercase.test.js +52 -0
  48. package/tests/rules/methods-naming.test.js +70 -0
  49. package/tests/rules/methods-ordering.test.js +75 -0
  50. package/tests/rules/multiline-comment-end-backslash.test.js +52 -0
  51. package/tests/rules/newline-after-switch-case.test.js +75 -0
  52. package/tests/rules/no-async.test.js +57 -0
  53. package/tests/rules/no-extra-line-within-function.test.js +87 -0
  54. package/tests/rules/no-inline-function-in-ternary.test.js +59 -0
  55. package/tests/rules/no-short-parameters.test.js +62 -0
  56. package/tests/rules/no-snake-case.test.js +81 -0
  57. package/tests/rules/no-space-in-optional-arguments.test.js +40 -0
  58. package/tests/rules/no-useless-template-literals.test.js +36 -0
  59. package/tests/rules/no-var-in-blocks.test.js +40 -0
  60. package/tests/rules/one-space-after-operator.test.js +69 -0
  61. package/tests/rules/regex-in-constructor.test.js +40 -0
  62. package/tests/rules/ternary-parenthesis.test.js +34 -0
  63. package/tests/rules/two-lines-between-class-members.test.js +62 -0
  64. package/tests/rules/variable-names.test.js +60 -0
  65. package/tests/rules/vue-attribute-comma.test.js +43 -0
  66. package/tests/rules/vue-attribute-linebreak.test.js +54 -0
  67. package/tests/rules/vue-class-attribute-last.test.js +59 -0
  68. package/tests/rules/vue-class-shorthand-max-len.test.js +67 -0
  69. package/tests/rules/vue-computed-order.test.js +27 -0
  70. package/tests/rules/vue-data-comment.test.js +61 -0
  71. package/tests/rules/vue-define-component.test.js +45 -0
  72. package/tests/rules/vue-emits-order.test.js +27 -0
  73. package/tests/rules/vue-header-check.test.js +49 -0
  74. package/tests/rules/vue-html-indent.test.js +77 -0
  75. package/tests/rules/vue-html-quotes.test.js +71 -0
  76. package/tests/rules/vue-methods-separator.test.js +55 -0
  77. package/tests/rules/vue-name-prop.test.js +59 -0
  78. package/tests/rules/vue-no-component-class-shorthand.test.js +49 -0
  79. package/tests/rules/vue-no-mixed-class.test.js +56 -0
  80. package/tests/rules/vue-no-name-option.test.js +44 -0
  81. package/tests/rules/vue-no-regex-data.test.js +39 -0
  82. package/tests/rules/vue-pinia-action-single-parameter.test.js +234 -0
  83. package/tests/rules/vue-props-declaration-line-break.test.js +68 -0
  84. package/tests/rules/vue-props-declaration-multiline.test.js +40 -0
  85. package/tests/rules/vue-props-declaration-order.test.js +27 -0
  86. package/tests/rules/vue-ref-case.test.js +40 -0
  87. package/tests/rules/vue-watch-handler.test.js +89 -0
  88. 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) | Disallow parameter names shorter than 3 characters | 🟠 | 🟢 | 🟣 |
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.5.13",
3
+ "version": "1.6.1",
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",
@@ -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",
@@ -52,9 +52,12 @@ export default {
52
52
 
53
53
  fix(fixer) {
54
54
  const indent = " ".repeat(nodeIndent);
55
- const commentText = sourceCode.getText(comment);
56
- const fixedComment = `\n${indent}${commentText.trim()}`;
57
- return fixer.replaceTextRange([comment.range[0], comment.range[1]], fixedComment);
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
- const varName = node.left.property.name;
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,
@@ -55,9 +55,15 @@ export default {
55
55
 
56
56
  if (headerStart && headerEnd) {
57
57
  // Construct the final headerFormat RegExps
58
- const headerFormats = basePatterns.map(pattern => headerStart && headerEnd ? new RegExp("^" + headerStart + pattern + headerEnd) : null);
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 && format.test(fileContent)) === false) {
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 {};
@@ -221,25 +221,22 @@ export default {
221
221
  break;
222
222
  }
223
223
 
224
- case "ExportDefaultDeclaration": {
225
- // Only enforce the 'EXPORTS' comment block if it's the last statement in the file
226
- if (startNode !== context.getSourceCode().ast.body.slice(-1)[0] && (!comment || `/*${comment.value.trim()}*/` !== COMMENT_HEADER_FORMAT("EXPORTS"))) {
227
- context.report({
228
- node: startNode,
229
- message: "Export group must be preceded by a 'EXPORTS' comment block",
230
- });
231
- }
232
-
233
- break;
234
- }
235
224
  }
236
225
  }
237
226
 
238
227
  return {
239
228
  ":statement": (node) => {
229
+ if (node.parent.type !== "Program") {
230
+ return;
231
+ }
232
+
240
233
  // If the type of node has changed since last time, or if we have a \
241
234
  // regex declaration and the previous declaration wasn't one
242
- if (node.type !== lastNode || isRegex(node) !== isRegex(lastNode)) {
235
+ if (
236
+ !lastNode ||
237
+ node.type !== lastNode.type ||
238
+ isRegex(node) !== isRegex(lastNode)
239
+ ) {
243
240
  // Check the group starting with the last node
244
241
  if (groupStart) {
245
242
  checkGroup(lastNode.type, groupStart);
@@ -29,9 +29,14 @@ function isIgnoreComment(comment) {
29
29
  }
30
30
 
31
31
  function getDirectoryName(filePath) {
32
- const pathParts = filePath.split("/");
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 pathParts[pathParts.length - 2].toUpperCase();
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).map(([regexStr, group]) => {
74
- return [new RegExp(regexStr), group];
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
- if (comments.length > 0) {
21
- const lastComment = comments[comments.length - 1];
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
- if (lastComment.type === "Block" && lastComment.value.startsWith("*")) {
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
- sourceCode.getIndexFromLoc({ line: lineOfPreviousNodeEnd, column: 0 }),
42
- sourceCode.getIndexFromLoc({ line: lineOfCurrentNode, column: 0 })
43
- ];
44
- return fixer.replaceTextRange(range, "\n\n");
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
  }
@@ -2,7 +2,7 @@ export default {
2
2
  meta: {
3
3
  type: "suggestion",
4
4
  docs: {
5
- description: "disallow short parameter names",
5
+ description: "disallow one-character parameter names",
6
6
  category: "Stylistic Issues",
7
7
  },
8
8
  schema: [
@@ -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 (sourceCode.getTokenBefore(operatorToken).end !== operatorToken.start || sourceCode.getTokenAfter(operatorToken).start !== operatorToken.end) {
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 = (node, operatorToken, operatorName) => {
31
- if (Math.abs(sourceCode.getTokenBefore(operatorToken).loc.end.column - operatorToken.loc.start.column) < 1 || sourceCode.getTokenAfter(operatorToken).loc.start.column - operatorToken.loc.end.column > 1) {
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 '${operatorName}'`,
51
+ message: `There should be at least one space before and exactly one space after '${operatorToken.value}'`,
35
52
 
36
53
  fix(fixer) {
37
- return fixer.replaceTextRange(
38
- [sourceCode.getTokenBefore(operatorToken).range[1], sourceCode.getTokenAfter(operatorToken).range[0]],
39
- ` ${operatorName} `
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.getTokenBefore(node.right);
49
- checkSpacing(node, operatorToken, "=");
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.getTokenBefore(node.init);
54
- checkSpacing(node, operatorToken, "=");
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.getTokenBefore(node.value);
61
- checkSpacing(node, operatorToken, ":");
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(`\\n\\n<!--\\s*\\*{70,73}\\s*\\n\\s{5}${headers[index]}\\s*\\n\\s*\\*{70,73}\\s*-->\\s*\\n\\n<${tag}`);
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)) {
@@ -5,8 +5,7 @@ export default {
5
5
  description: "disallow regex in Vue data object",
6
6
  category: "Possible Errors",
7
7
  recommended: true,
8
- },
9
- fixable: "code",
8
+ }
10
9
  },
11
10
 
12
11
  create(context) {
@@ -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
+ }