eslint-plugin-crisp 1.7.11 → 1.7.13

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/README.md CHANGED
@@ -227,6 +227,7 @@ Each item has emojis denoting:
227
227
  | [crisp/vue-computed-order](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/vue-computed-order.js) | Ensures computed properties are alphabetically ordered | | 🟢 |
228
228
  | [crisp/vue-define-component](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/vue-define-component.js) | Enforces mixins and components to use the `defineComponent` wrapper | | 🟢 |
229
229
  | [crisp/vue-emits-order](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/vue-emits-order.js) | Ensures emits properties are alphabetically ordered | | 🟢 |
230
+ | [crisp/vue-event-listener-jsdoc](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/vue-event-listener-jsdoc.js) | Requires event listener method JSDoc descriptions to start with `Triggers on` | | 🟢 |
230
231
  | [crisp/vue-header-check](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/vue-header-check.js) | Ensures `script`, `template` and `style` tags start with corresponding comment block | | 🟢 |
231
232
  | [crisp/vue-html-indent](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/vue-html-indent.js) | Enforces consistent indentation in `template` (supports for Pug) | | 🟢 |
232
233
  | [crisp/vue-html-quotes](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/vue-html-quotes.js) | Enforces HTML attributes to be enclosed with double quotes | | 🟢 |
package/index.js CHANGED
@@ -50,6 +50,7 @@ import ruleVueComputedOrder from "./rules/vue-computed-order.js";
50
50
  import ruleVueDataComment from "./rules/vue-data-comment.js";
51
51
  import ruleVueDefineComponent from "./rules/vue-define-component.js";
52
52
  import ruleVueEmitsOrder from "./rules/vue-emits-order.js";
53
+ import ruleVueEventListenerJsdoc from "./rules/vue-event-listener-jsdoc.js";
53
54
  import ruleVueHeaderCheck from "./rules/vue-header-check.js";
54
55
  import ruleVueHtmlIndent from "./rules/vue-html-indent.js";
55
56
  import ruleVueHtmlQuotes from "./rules/vue-html-quotes.js";
@@ -129,6 +130,7 @@ const plugin = {
129
130
  "vue-data-comment": ruleVueDataComment,
130
131
  "vue-define-component": ruleVueDefineComponent,
131
132
  "vue-emits-order": ruleVueEmitsOrder,
133
+ "vue-event-listener-jsdoc": ruleVueEventListenerJsdoc,
132
134
  "vue-header-check": ruleVueHeaderCheck,
133
135
  "vue-html-indent": ruleVueHtmlIndent,
134
136
  "vue-html-quotes": ruleVueHtmlQuotes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-crisp",
3
- "version": "1.7.11",
3
+ "version": "1.7.13",
4
4
  "description": "Custom ESLint Rules for Crisp",
5
5
  "author": "Crisp IM SAS",
6
6
  "main": "index.js",
@@ -385,6 +385,7 @@ export default function configRecommendedVue(pluginCrisp) {
385
385
  "crisp/vue-data-comment": "error",
386
386
  "crisp/vue-define-component": "error",
387
387
  "crisp/vue-emits-order": "error",
388
+ "crisp/vue-event-listener-jsdoc": "error",
388
389
  "crisp/vue-header-check": "error",
389
390
  "crisp/vue-html-indent": "error",
390
391
  "crisp/vue-html-quotes": "error",
@@ -0,0 +1,87 @@
1
+ import doctrine from "doctrine";
2
+
3
+ const EVENT_LISTENERS_SEPARATOR = "--> EVENT LISTENERS <--";
4
+ const DESCRIPTION_PREFIX = "Triggers on ";
5
+
6
+ export default {
7
+ meta: {
8
+ type: "suggestion",
9
+ docs: {
10
+ description:
11
+ "require event listener method descriptions to start with 'Triggers on'",
12
+ category: "Stylistic Issues",
13
+ recommended: false
14
+ },
15
+ fixable: null,
16
+ schema: [],
17
+ messages: {
18
+ invalidDescription:
19
+ "Event listener method JSDoc description must start with " +
20
+ "'Triggers on ...'.",
21
+ missingJsdoc: "Event listener methods must have a JSDoc comment."
22
+ }
23
+ },
24
+
25
+ create(context) {
26
+ const sourceCode = context.sourceCode;
27
+
28
+ function checkMethods(node) {
29
+ const properties = node.value.properties;
30
+
31
+ if (!properties) {
32
+ return;
33
+ }
34
+
35
+ let eventListenersStarted = false;
36
+
37
+ properties.forEach((property) => {
38
+ const comments = sourceCode.getCommentsBefore(property);
39
+
40
+ if (!eventListenersStarted) {
41
+ eventListenersStarted = comments.some((comment) => {
42
+ return (
43
+ comment.type === "Line" &&
44
+ comment.value.trim() === EVENT_LISTENERS_SEPARATOR
45
+ );
46
+ });
47
+ }
48
+
49
+ if (!eventListenersStarted || property.type !== "Property") {
50
+ return;
51
+ }
52
+
53
+ const jsdoc = [...comments].reverse().find((comment) => {
54
+ return comment.type === "Block" && comment.value.startsWith("*");
55
+ });
56
+
57
+ if (!jsdoc) {
58
+ context.report({
59
+ node: property,
60
+ messageId: "missingJsdoc"
61
+ });
62
+
63
+ return;
64
+ }
65
+
66
+ const description = doctrine.parse(
67
+ `/*${jsdoc.value}*/`,
68
+ { unwrap: true }
69
+ ).description.trim();
70
+
71
+ if (!description.startsWith(DESCRIPTION_PREFIX)) {
72
+ context.report({
73
+ node: jsdoc,
74
+ messageId: "invalidDescription"
75
+ });
76
+ }
77
+ });
78
+ }
79
+
80
+ return {
81
+ 'ExportDefaultDeclaration > ObjectExpression > Property[key.name="methods"]':
82
+ checkMethods,
83
+ 'ExportDefaultDeclaration > CallExpression[callee.name="defineComponent"] > ObjectExpression > Property[key.name="methods"]':
84
+ checkMethods
85
+ };
86
+ }
87
+ };
@@ -18,7 +18,7 @@ export default {
18
18
  category: "Stylistic Issues",
19
19
  recommended: false
20
20
  },
21
- fixable: null,
21
+ fixable: "code",
22
22
  schema: [],
23
23
  messages: {
24
24
  duplicateSeparator:
@@ -39,6 +39,114 @@ export default {
39
39
  create(context) {
40
40
  const sourceCode = context.sourceCode;
41
41
 
42
+ function createOrderFix(node, separatorEntries, properties) {
43
+ const objectEnd = node.value.range[1] - 1;
44
+ const sectionStarts = separatorEntries.map(({ comment }) => {
45
+ return sourceCode.getIndexFromLoc({
46
+ line: comment.loc.start.line,
47
+ column: 0
48
+ });
49
+ });
50
+
51
+ const hasInlineSeparator = separatorEntries.some(
52
+ ({ comment }, index) => {
53
+ return sourceCode.text
54
+ .slice(sectionStarts[index], comment.range[0])
55
+ .trim() !== "";
56
+ }
57
+ );
58
+
59
+ if (hasInlineSeparator) {
60
+ return null;
61
+ }
62
+
63
+ const sections = separatorEntries.map((entry, index) => {
64
+ const start = sectionStarts[index];
65
+ const end = sectionStarts[index + 1] ?? objectEnd;
66
+ const rawText = sourceCode.text.slice(start, end);
67
+ const text = rawText.trimEnd();
68
+ const nextEntry = separatorEntries[index + 1];
69
+ const sectionProperties = properties.filter((property) => {
70
+ return (
71
+ property.range[0] > entry.comment.range[1] &&
72
+ (!nextEntry || property.range[0] < nextEntry.comment.range[0])
73
+ );
74
+ });
75
+
76
+ return {
77
+ ...entry,
78
+ start,
79
+ end,
80
+ text,
81
+ trailingWhitespace: rawText.slice(text.length),
82
+ properties: sectionProperties
83
+ };
84
+ });
85
+
86
+ if (sections.some((section) => section.properties.length === 0)) {
87
+ return null;
88
+ }
89
+
90
+ const sectionGap = sections[0].trailingWhitespace;
91
+
92
+ if (!/[\r\n]/.test(sectionGap)) {
93
+ return null;
94
+ }
95
+
96
+ const suffix = sections.at(-1).trailingWhitespace;
97
+ const lastProperty = properties.at(-1);
98
+ const tokenAfterLastProperty = sourceCode.getTokenAfter(lastProperty);
99
+ const preserveTrailingComma = (
100
+ tokenAfterLastProperty?.value === "," &&
101
+ tokenAfterLastProperty.range[0] < objectEnd
102
+ );
103
+ const orderedSections = [...sections].sort((first, second) => {
104
+ return first.order - second.order;
105
+ });
106
+
107
+ const fixedSections = orderedSections.map((section, index) => {
108
+ const lastSectionProperty = section.properties.at(-1);
109
+ const tokenAfterProperty = sourceCode.getTokenAfter(
110
+ lastSectionProperty
111
+ );
112
+ const hasComma = (
113
+ tokenAfterProperty?.value === "," &&
114
+ tokenAfterProperty.range[0] < section.end
115
+ );
116
+ const needsComma = (
117
+ index < orderedSections.length - 1 ||
118
+ preserveTrailingComma
119
+ );
120
+ const propertyEnd = lastSectionProperty.range[1] - section.start;
121
+
122
+ if (needsComma && !hasComma) {
123
+ return (
124
+ section.text.slice(0, propertyEnd) +
125
+ "," +
126
+ section.text.slice(propertyEnd)
127
+ );
128
+ }
129
+
130
+ if (!needsComma && hasComma) {
131
+ const commaStart = tokenAfterProperty.range[0] - section.start;
132
+
133
+ return (
134
+ section.text.slice(0, commaStart) +
135
+ section.text.slice(commaStart + 1)
136
+ );
137
+ }
138
+
139
+ return section.text;
140
+ });
141
+
142
+ return (fixer) => {
143
+ return fixer.replaceTextRange(
144
+ [sectionStarts[0], objectEnd],
145
+ fixedSections.join(sectionGap) + suffix
146
+ );
147
+ };
148
+ }
149
+
42
150
  function checkMethods(node) {
43
151
  const properties = node.value.properties;
44
152
 
@@ -57,8 +165,10 @@ export default {
57
165
  }
58
166
  );
59
167
  const seenSeparators = new Set();
60
- let lastSeparatorOrder = -1;
168
+ const separatorEntries = [];
61
169
  let separatorCount = 0;
170
+ let hasDuplicateSeparator = false;
171
+ let hasInvalidSeparator = false;
62
172
 
63
173
  comments.forEach((comment) => {
64
174
  if (comment.type !== "Line") {
@@ -70,6 +180,8 @@ export default {
70
180
 
71
181
  if (separatorOrder === undefined) {
72
182
  if (SEPARATOR_LIKE_PATTERN.test(separator)) {
183
+ hasInvalidSeparator = true;
184
+
73
185
  context.report({
74
186
  node: comment,
75
187
  messageId: "invalidSeparator"
@@ -82,6 +194,8 @@ export default {
82
194
  separatorCount++;
83
195
 
84
196
  if (seenSeparators.has(separator)) {
197
+ hasDuplicateSeparator = true;
198
+
85
199
  context.report({
86
200
  node: comment,
87
201
  messageId: "duplicateSeparator",
@@ -92,15 +206,37 @@ export default {
92
206
  }
93
207
 
94
208
  seenSeparators.add(separator);
209
+ separatorEntries.push({
210
+ comment,
211
+ order: separatorOrder
212
+ });
213
+ });
95
214
 
96
- if (separatorOrder < lastSeparatorOrder) {
97
- context.report({
98
- node: comment,
99
- messageId: "unorderedSeparator"
100
- });
101
- } else {
102
- lastSeparatorOrder = separatorOrder;
215
+ let lastSeparatorOrder = -1;
216
+ const unorderedEntries = separatorEntries.filter((entry) => {
217
+ if (entry.order < lastSeparatorOrder) {
218
+ return true;
103
219
  }
220
+
221
+ lastSeparatorOrder = entry.order;
222
+
223
+ return false;
224
+ });
225
+
226
+ const orderFix = (
227
+ unorderedEntries.length > 0 &&
228
+ !hasDuplicateSeparator &&
229
+ !hasInvalidSeparator
230
+ )
231
+ ? createOrderFix(node, separatorEntries, properties)
232
+ : null;
233
+
234
+ unorderedEntries.forEach((entry, index) => {
235
+ context.report({
236
+ node: entry.comment,
237
+ messageId: "unorderedSeparator",
238
+ ...(index === 0 && orderFix ? { fix: orderFix } : {})
239
+ });
104
240
  });
105
241
 
106
242
  if (separatorCount === 0) {
@@ -0,0 +1,174 @@
1
+ import rule from "../../rules/vue-event-listener-jsdoc.js";
2
+ import { createVueRuleTester } from "../rule-tester.js";
3
+
4
+ const ruleTester = createVueRuleTester();
5
+
6
+ ruleTester.run("vue-event-listener-jsdoc", rule, {
7
+ valid: [
8
+ {
9
+ code: "export default { methods: {} };"
10
+ },
11
+ {
12
+ code: `
13
+ export default {
14
+ methods: {
15
+ // --> HELPERS <--
16
+ /**
17
+ * Handles formatting
18
+ */
19
+ format() {}
20
+ }
21
+ };
22
+ `
23
+ },
24
+ {
25
+ code: `
26
+ export default {
27
+ methods: {
28
+ // --> EVENT LISTENERS <--
29
+ /**
30
+ * Triggers on load more
31
+ * @public
32
+ * @param {object} $state
33
+ */
34
+ onLoadMore($state) {}
35
+ }
36
+ };
37
+ `
38
+ },
39
+ {
40
+ code: `
41
+ export default defineComponent({
42
+ methods: {
43
+ // --> ACTIONS <--
44
+ /**
45
+ * Saves content
46
+ */
47
+ save() {},
48
+ // --> HELPERS <--
49
+ /**
50
+ * Formats content
51
+ */
52
+ format() {},
53
+ // --> EVENT LISTENERS <--
54
+ /**
55
+ * Triggers on save
56
+ */
57
+ onSave() {},
58
+ /**
59
+ * Triggers on close
60
+ */
61
+ onClose: function() {}
62
+ }
63
+ });
64
+ `
65
+ },
66
+ {
67
+ filename: "Component.vue",
68
+ code: `
69
+ <script>
70
+ export default defineComponent({
71
+ methods: {
72
+ // --> EVENT LISTENERS <--
73
+ /**
74
+ * Triggers on load more
75
+ * @public
76
+ */
77
+ onLoadMore() {
78
+ this.loadMore(() => {
79
+ /**
80
+ * Handles a nested callback
81
+ */
82
+ return true;
83
+ });
84
+ }
85
+ }
86
+ });
87
+ </script>
88
+ `
89
+ }
90
+ ],
91
+
92
+ invalid: [
93
+ {
94
+ code: `
95
+ export default {
96
+ methods: {
97
+ // --> EVENT LISTENERS <--
98
+ /**
99
+ * Handles load more
100
+ * @public
101
+ */
102
+ onLoadMore() {}
103
+ }
104
+ };
105
+ `,
106
+ errors: [
107
+ {
108
+ messageId: "invalidDescription"
109
+ }
110
+ ]
111
+ },
112
+ {
113
+ code: `
114
+ export default {
115
+ methods: {
116
+ // --> EVENT LISTENERS <--
117
+ onLoadMore() {}
118
+ }
119
+ };
120
+ `,
121
+ errors: [
122
+ {
123
+ messageId: "missingJsdoc"
124
+ }
125
+ ]
126
+ },
127
+ {
128
+ code: `
129
+ export default {
130
+ methods: {
131
+ // --> EVENT LISTENERS <--
132
+ /**
133
+ * Triggers on
134
+ * @public
135
+ */
136
+ onLoadMore() {}
137
+ }
138
+ };
139
+ `,
140
+ errors: [
141
+ {
142
+ messageId: "invalidDescription"
143
+ }
144
+ ]
145
+ },
146
+ {
147
+ filename: "Component.vue",
148
+ code: `
149
+ <script>
150
+ export default defineComponent({
151
+ methods: {
152
+ // --> EVENT LISTENERS <--
153
+ /**
154
+ * Triggers when content is saved
155
+ * @public
156
+ */
157
+ onSave() {},
158
+ /**
159
+ * Triggers on close
160
+ * @public
161
+ */
162
+ onClose() {}
163
+ }
164
+ });
165
+ </script>
166
+ `,
167
+ errors: [
168
+ {
169
+ messageId: "invalidDescription"
170
+ }
171
+ ]
172
+ }
173
+ ]
174
+ });
@@ -149,12 +149,94 @@ ruleTester.run("vue-methods-separator", rule, {
149
149
  }
150
150
  };
151
151
  `,
152
+ output: `
153
+ export default {
154
+ methods: {
155
+ // --> ACTIONS <--
156
+ save() {},
157
+ // --> HELPERS <--
158
+ format() {}
159
+ }
160
+ };
161
+ `,
152
162
  errors: [
153
163
  {
154
164
  messageId: "unorderedSeparator"
155
165
  }
156
166
  ]
157
167
  },
168
+ {
169
+ code: `
170
+ export default {
171
+ methods: {
172
+ // --> EVENT LISTENERS <--
173
+
174
+ /**
175
+ * Triggers on save
176
+ */
177
+ onSave() {},
178
+
179
+ /**
180
+ * Triggers on close
181
+ */
182
+ onClose() {},
183
+
184
+ // --> ACTIONS <--
185
+
186
+ /**
187
+ * Saves content
188
+ */
189
+ save() {},
190
+
191
+ // --> HELPERS <--
192
+
193
+ /**
194
+ * Formats content
195
+ */
196
+ format() {}
197
+ }
198
+ };
199
+ `,
200
+ output: `
201
+ export default {
202
+ methods: {
203
+ // --> ACTIONS <--
204
+
205
+ /**
206
+ * Saves content
207
+ */
208
+ save() {},
209
+
210
+ // --> HELPERS <--
211
+
212
+ /**
213
+ * Formats content
214
+ */
215
+ format() {},
216
+
217
+ // --> EVENT LISTENERS <--
218
+
219
+ /**
220
+ * Triggers on save
221
+ */
222
+ onSave() {},
223
+
224
+ /**
225
+ * Triggers on close
226
+ */
227
+ onClose() {}
228
+ }
229
+ };
230
+ `,
231
+ errors: [
232
+ {
233
+ messageId: "unorderedSeparator"
234
+ },
235
+ {
236
+ messageId: "unorderedSeparator"
237
+ }
238
+ ]
239
+ },
158
240
  {
159
241
  code: `
160
242
  export default {
@@ -184,6 +266,16 @@ ruleTester.run("vue-methods-separator", rule, {
184
266
  }
185
267
  });
186
268
  `,
269
+ output: `
270
+ export default defineComponent({
271
+ methods: {
272
+ // --> HELPERS <--
273
+ format() {},
274
+ // --> EVENT LISTENERS <--
275
+ onSave() {}
276
+ }
277
+ });
278
+ `,
187
279
  errors: [
188
280
  {
189
281
  messageId: "unorderedSeparator"
@@ -220,6 +312,18 @@ ruleTester.run("vue-methods-separator", rule, {
220
312
  });
221
313
  </script>
222
314
  `,
315
+ output: `
316
+ <script>
317
+ export default defineComponent({
318
+ methods: {
319
+ // --> ACTIONS <--
320
+ save() {},
321
+ // --> EVENT LISTENERS <--
322
+ onSave() {}
323
+ }
324
+ });
325
+ </script>
326
+ `,
223
327
  errors: [
224
328
  {
225
329
  messageId: "unorderedSeparator"