eslint-plugin-crisp 1.7.14 → 1.7.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-crisp",
3
- "version": "1.7.14",
3
+ "version": "1.7.16",
4
4
  "description": "Custom ESLint Rules for Crisp",
5
5
  "author": "Crisp IM SAS",
6
6
  "main": "index.js",
@@ -1,5 +1,6 @@
1
1
  const EVENT_LISTENERS_SEPARATOR = "--> EVENT LISTENERS <--";
2
2
  const EVENT_LISTENER_NAME_PATTERN = /^on[A-Z]/;
3
+ const SEPARATOR_LIKE_PATTERN = /^--> .+ <--$/;
3
4
 
4
5
  export default {
5
6
  meta: {
@@ -10,7 +11,7 @@ export default {
10
11
  category: "Stylistic Issues",
11
12
  recommended: false
12
13
  },
13
- fixable: null,
14
+ fixable: "code",
14
15
  schema: [],
15
16
  messages: {
16
17
  wrongSection:
@@ -21,6 +22,13 @@ export default {
21
22
  create(context) {
22
23
  const sourceCode = context.sourceCode;
23
24
 
25
+ function getLineStart(node) {
26
+ return sourceCode.getIndexFromLoc({
27
+ line: node.loc.start.line,
28
+ column: 0
29
+ });
30
+ }
31
+
24
32
  function getPropertyName(property) {
25
33
  if (property.type !== "Property" || property.computed) {
26
34
  return null;
@@ -40,6 +48,188 @@ export default {
40
48
  return null;
41
49
  }
42
50
 
51
+ function getTopLevelComments(node, properties) {
52
+ return sourceCode.getCommentsInside(node.value).filter((comment) => {
53
+ return !properties.some((property) => {
54
+ return (
55
+ comment.range[0] >= property.range[0] &&
56
+ comment.range[1] <= property.range[1]
57
+ );
58
+ });
59
+ });
60
+ }
61
+
62
+ function getMethodBlock(node, property) {
63
+ const comments = sourceCode.getCommentsBefore(property);
64
+ const lastSeparatorIndex = comments.findLastIndex((comment) => {
65
+ return (
66
+ comment.type === "Line" &&
67
+ SEPARATOR_LIKE_PATTERN.test(comment.value.trim())
68
+ );
69
+ });
70
+ const ownedComments = comments.slice(lastSeparatorIndex + 1);
71
+ const firstNode = ownedComments[0] || property;
72
+ const start = getLineStart(firstNode);
73
+ const linePrefix = sourceCode.text.slice(start, firstNode.range[0]);
74
+
75
+ if (linePrefix.trim() !== "") {
76
+ return null;
77
+ }
78
+
79
+ const tokenAfter = sourceCode.getTokenAfter(property);
80
+ const comma = (
81
+ tokenAfter?.value === "," &&
82
+ tokenAfter.range[0] < node.value.range[1]
83
+ )
84
+ ? tokenAfter
85
+ : null;
86
+ const propertyEnd = comma?.range[1] || property.range[1];
87
+ const remainingText = sourceCode.text.slice(
88
+ propertyEnd,
89
+ node.value.range[1] - 1
90
+ );
91
+ const nextContentMatch = remainingText.match(/\S/);
92
+ let removalEnd = propertyEnd;
93
+
94
+ if (nextContentMatch) {
95
+ const nextContent = propertyEnd + nextContentMatch.index;
96
+ const nextContentLocation = sourceCode.getLocFromIndex(nextContent);
97
+
98
+ if (nextContentLocation.line > property.loc.end.line) {
99
+ removalEnd = sourceCode.getIndexFromLoc({
100
+ line: nextContentLocation.line,
101
+ column: 0
102
+ });
103
+ }
104
+ }
105
+
106
+ return {
107
+ start,
108
+ end: removalEnd,
109
+ text: sourceCode.text.slice(start, property.range[1]).trimEnd()
110
+ };
111
+ }
112
+
113
+ function createMoveFix(node, properties, violations) {
114
+ const topLevelComments = getTopLevelComments(node, properties);
115
+ const eventSeparators = topLevelComments.filter((comment) => {
116
+ return (
117
+ comment.type === "Line" &&
118
+ comment.value.trim() === EVENT_LISTENERS_SEPARATOR
119
+ );
120
+ });
121
+
122
+ if (eventSeparators.length > 1) {
123
+ return null;
124
+ }
125
+
126
+ const eventSeparator = eventSeparators[0] || null;
127
+ const hasSectionAfterEvent = eventSeparator && topLevelComments.some(
128
+ (comment) => {
129
+ return (
130
+ comment.range[0] > eventSeparator.range[1] &&
131
+ comment.type === "Line" &&
132
+ SEPARATOR_LIKE_PATTERN.test(comment.value.trim())
133
+ );
134
+ }
135
+ );
136
+
137
+ if (hasSectionAfterEvent) {
138
+ return null;
139
+ }
140
+
141
+ const lastProperty = properties.at(-1);
142
+ const trailingComments = topLevelComments.filter((comment) => {
143
+ return comment.range[0] > lastProperty.range[1];
144
+ });
145
+ const hasUnsafeTrailingComment = trailingComments.some((comment) => {
146
+ return comment !== eventSeparator;
147
+ });
148
+
149
+ if (hasUnsafeTrailingComment) {
150
+ return null;
151
+ }
152
+
153
+ const methodBlocks = violations.map(({ property }) => {
154
+ return getMethodBlock(node, property);
155
+ });
156
+
157
+ if (methodBlocks.some((block) => block === null)) {
158
+ return null;
159
+ }
160
+
161
+ const movedProperties = new Set(
162
+ violations.map(({ property }) => property)
163
+ );
164
+ const remainingProperties = properties.filter((property) => {
165
+ return !movedProperties.has(property);
166
+ });
167
+ const interiorStart = node.value.range[0] + 1;
168
+ const interiorEnd = node.value.range[1] - 1;
169
+ const interiorText = sourceCode.text.slice(interiorStart, interiorEnd);
170
+ const suffixMatch = interiorText.match(/\s*$/);
171
+ const suffix = suffixMatch?.[0] || "";
172
+ const edits = methodBlocks.map((block) => {
173
+ return {
174
+ start: block.start - interiorStart,
175
+ end: block.end - interiorStart,
176
+ text: ""
177
+ };
178
+ });
179
+
180
+ if (remainingProperties.length > 0) {
181
+ const lastRemainingProperty = remainingProperties.at(-1);
182
+ const tokenAfter = sourceCode.getTokenAfter(lastRemainingProperty);
183
+ const hasComma = (
184
+ tokenAfter?.value === "," &&
185
+ tokenAfter.range[0] < interiorEnd
186
+ );
187
+
188
+ if (!hasComma) {
189
+ const propertyEnd = lastRemainingProperty.range[1] - interiorStart;
190
+
191
+ edits.push({
192
+ start: propertyEnd,
193
+ end: propertyEnd,
194
+ text: ","
195
+ });
196
+ }
197
+ }
198
+
199
+ const transformedText = [...edits]
200
+ .sort((first, second) => {
201
+ return second.start - first.start || second.end - first.end;
202
+ })
203
+ .reduce((text, edit) => {
204
+ return (
205
+ text.slice(0, edit.start) +
206
+ edit.text +
207
+ text.slice(edit.end)
208
+ );
209
+ }, interiorText)
210
+ .trimEnd();
211
+ const movedText = methodBlocks
212
+ .map((block) => block.text)
213
+ .join(",\n\n");
214
+ const methodIndent = movedText.match(/^[\t ]*/)?.[0] || "";
215
+ const sectionText = eventSeparator
216
+ ? movedText
217
+ : (
218
+ `${methodIndent}// ${EVENT_LISTENERS_SEPARATOR}\n\n` +
219
+ movedText
220
+ );
221
+ const fixedInterior = transformedText === ""
222
+ ? `\n${sectionText}${suffix}`
223
+ : `${transformedText}\n\n${sectionText}${suffix}`;
224
+
225
+ return (fixer) => {
226
+ return fixer.replaceTextRange(
227
+ [interiorStart, interiorEnd],
228
+ fixedInterior
229
+ );
230
+ };
231
+ }
232
+
43
233
  function checkMethods(node) {
44
234
  const properties = node.value.properties;
45
235
 
@@ -48,6 +238,7 @@ export default {
48
238
  }
49
239
 
50
240
  let eventListenersStarted = false;
241
+ const violations = [];
51
242
 
52
243
  properties.forEach((property) => {
53
244
  const comments = sourceCode.getCommentsBefore(property);
@@ -68,13 +259,25 @@ export default {
68
259
  const name = getPropertyName(property);
69
260
 
70
261
  if (name && EVENT_LISTENER_NAME_PATTERN.test(name)) {
71
- context.report({
72
- node: property.key,
73
- messageId: "wrongSection",
74
- data: { name }
262
+ violations.push({
263
+ property,
264
+ name
75
265
  });
76
266
  }
77
267
  });
268
+
269
+ const moveFix = violations.length > 0
270
+ ? createMoveFix(node, properties, violations)
271
+ : null;
272
+
273
+ violations.forEach(({ property, name }, index) => {
274
+ context.report({
275
+ node: property.key,
276
+ messageId: "wrongSection",
277
+ data: { name },
278
+ ...(index === 0 && moveFix ? { fix: moveFix } : {})
279
+ });
280
+ });
78
281
  }
79
282
 
80
283
  return {
@@ -30,6 +30,8 @@ export default {
30
30
  "Methods block must contain at least one separator comment: " +
31
31
  "'// --> ACTIONS <--', '// --> HELPERS <--', " +
32
32
  "or '// --> EVENT LISTENERS <--'.",
33
+ missingSeparatorSpacing:
34
+ "Methods separator comments must be followed by a blank line.",
33
35
  unorderedSeparator:
34
36
  "Methods separator comments must be ordered: '// --> ACTIONS <--', " +
35
37
  "then '// --> HELPERS <--', then '// --> EVENT LISTENERS <--'."
@@ -147,6 +149,75 @@ export default {
147
149
  };
148
150
  }
149
151
 
152
+ function checkSeparatorSpacing(comment, comments, properties) {
153
+ const nextSeparator = comments.find((candidate) => {
154
+ return (
155
+ candidate.range[0] > comment.range[1] &&
156
+ candidate.type === "Line" &&
157
+ SEPARATOR_LIKE_PATTERN.test(candidate.value.trim())
158
+ );
159
+ });
160
+ const firstProperty = properties.find((property) => {
161
+ return (
162
+ property.range[0] > comment.range[1] &&
163
+ (!nextSeparator || property.range[0] < nextSeparator.range[0])
164
+ );
165
+ });
166
+
167
+ if (!firstProperty) {
168
+ return;
169
+ }
170
+
171
+ const firstLeadingComment = sourceCode
172
+ .getCommentsBefore(firstProperty)
173
+ .find((candidate) => {
174
+ return (
175
+ candidate.range[0] > comment.range[1] &&
176
+ (!nextSeparator || candidate.range[0] < nextSeparator.range[0])
177
+ );
178
+ });
179
+ const firstNode = firstLeadingComment || firstProperty;
180
+
181
+ if (firstNode.loc.start.line > comment.loc.end.line + 1) {
182
+ return;
183
+ }
184
+
185
+ const spacingFix = (fixer) => {
186
+ const lineStart = sourceCode.getIndexFromLoc({
187
+ line: firstNode.loc.start.line,
188
+ column: 0
189
+ });
190
+ const indentation = sourceCode.text.slice(
191
+ lineStart,
192
+ firstNode.range[0]
193
+ );
194
+ const currentSpacing = sourceCode.text.slice(
195
+ comment.range[1],
196
+ firstNode.range[0]
197
+ );
198
+
199
+ if (
200
+ indentation.trim() !== "" ||
201
+ currentSpacing.trim() !== ""
202
+ ) {
203
+ return null;
204
+ }
205
+
206
+ const linebreak = currentSpacing.includes("\r\n") ? "\r\n" : "\n";
207
+
208
+ return fixer.replaceTextRange(
209
+ [comment.range[1], firstNode.range[0]],
210
+ `${linebreak}${linebreak}${indentation}`
211
+ );
212
+ };
213
+
214
+ context.report({
215
+ node: comment,
216
+ messageId: "missingSeparatorSpacing",
217
+ fix: spacingFix
218
+ });
219
+ }
220
+
150
221
  function checkMethods(node) {
151
222
  const properties = node.value.properties;
152
223
 
@@ -171,11 +242,21 @@ export default {
171
242
  let hasInvalidSeparator = false;
172
243
 
173
244
  comments.forEach((comment) => {
245
+ const separator = comment.value.trim();
246
+
174
247
  if (comment.type !== "Line") {
248
+ if (SEPARATOR_LIKE_PATTERN.test(separator)) {
249
+ hasInvalidSeparator = true;
250
+
251
+ context.report({
252
+ node: comment,
253
+ messageId: "invalidSeparator"
254
+ });
255
+ }
256
+
175
257
  return;
176
258
  }
177
259
 
178
- const separator = comment.value.trim();
179
260
  const separatorOrder = SEPARATOR_ORDER.get(separator);
180
261
 
181
262
  if (separatorOrder === undefined) {
@@ -212,6 +293,17 @@ export default {
212
293
  });
213
294
  });
214
295
 
296
+ comments
297
+ .filter((comment) => {
298
+ return (
299
+ comment.type === "Line" &&
300
+ SEPARATOR_ORDER.has(comment.value.trim())
301
+ );
302
+ })
303
+ .forEach((comment) => {
304
+ checkSeparatorSpacing(comment, comments, properties);
305
+ });
306
+
215
307
  let lastSeparatorOrder = -1;
216
308
  const unorderedEntries = separatorEntries.filter((entry) => {
217
309
  if (entry.order < lastSeparatorOrder) {
@@ -72,6 +72,34 @@ ruleTester.run("vue-event-listener-section", rule, {
72
72
  export default {
73
73
  methods: {
74
74
  // --> ACTIONS <--
75
+
76
+ /**
77
+ * Saves content
78
+ */
79
+ save() {},
80
+
81
+ /**
82
+ * Triggers on save
83
+ */
84
+ onSave() {}
85
+ }
86
+ };
87
+ `,
88
+ output: `
89
+ export default {
90
+ methods: {
91
+ // --> ACTIONS <--
92
+
93
+ /**
94
+ * Saves content
95
+ */
96
+ save() {},
97
+
98
+ // --> EVENT LISTENERS <--
99
+
100
+ /**
101
+ * Triggers on save
102
+ */
75
103
  onSave() {}
76
104
  }
77
105
  };
@@ -88,6 +116,22 @@ ruleTester.run("vue-event-listener-section", rule, {
88
116
  export default {
89
117
  methods: {
90
118
  // --> HELPERS <--
119
+
120
+ format() {},
121
+
122
+ onLoadMore() {}
123
+ }
124
+ };
125
+ `,
126
+ output: `
127
+ export default {
128
+ methods: {
129
+ // --> HELPERS <--
130
+
131
+ format() {},
132
+
133
+ // --> EVENT LISTENERS <--
134
+
91
135
  onLoadMore() {}
92
136
  }
93
137
  };
@@ -107,6 +151,15 @@ ruleTester.run("vue-event-listener-section", rule, {
107
151
  }
108
152
  };
109
153
  `,
154
+ output: `
155
+ export default {
156
+ methods: {
157
+ // --> EVENT LISTENERS <--
158
+
159
+ "onClose": function() {}
160
+ }
161
+ };
162
+ `,
110
163
  errors: [
111
164
  {
112
165
  messageId: "wrongSection",
@@ -121,15 +174,45 @@ ruleTester.run("vue-event-listener-section", rule, {
121
174
  export default defineComponent({
122
175
  methods: {
123
176
  // --> ACTIONS <--
177
+
178
+ save() {},
179
+
124
180
  onSave() {},
125
181
  // --> HELPERS <--
182
+
183
+ format() {},
184
+
126
185
  onFormatComplete() {},
127
186
  // --> EVENT LISTENERS <--
187
+
128
188
  onClose() {}
129
189
  }
130
190
  });
131
191
  </script>
132
192
  `,
193
+ output: `
194
+ <script>
195
+ export default defineComponent({
196
+ methods: {
197
+ // --> ACTIONS <--
198
+
199
+ save() {},
200
+
201
+ // --> HELPERS <--
202
+
203
+ format() {},
204
+
205
+ // --> EVENT LISTENERS <--
206
+
207
+ onClose() {},
208
+
209
+ onSave() {},
210
+
211
+ onFormatComplete() {}
212
+ }
213
+ });
214
+ </script>
215
+ `,
133
216
  errors: [
134
217
  {
135
218
  messageId: "wrongSection",
@@ -13,6 +13,7 @@ ruleTester.run("vue-methods-separator", rule, {
13
13
  export default {
14
14
  methods: {
15
15
  // --> ACTIONS <--
16
+
16
17
  save() {}
17
18
  }
18
19
  };
@@ -23,6 +24,7 @@ ruleTester.run("vue-methods-separator", rule, {
23
24
  export default {
24
25
  methods: {
25
26
  // --> HELPERS <--
27
+
26
28
  format() {}
27
29
  }
28
30
  };
@@ -33,6 +35,7 @@ ruleTester.run("vue-methods-separator", rule, {
33
35
  export default {
34
36
  methods: {
35
37
  // --> EVENT LISTENERS <--
38
+
36
39
  onSave() {}
37
40
  }
38
41
  };
@@ -43,8 +46,10 @@ ruleTester.run("vue-methods-separator", rule, {
43
46
  export default {
44
47
  methods: {
45
48
  // --> ACTIONS <--
49
+
46
50
  save() {},
47
51
  // --> EVENT LISTENERS <--
52
+
48
53
  onSave() {}
49
54
  }
50
55
  };
@@ -55,10 +60,13 @@ ruleTester.run("vue-methods-separator", rule, {
55
60
  export default defineComponent({
56
61
  methods: {
57
62
  // --> ACTIONS <--
63
+
58
64
  save() {},
59
65
  // --> HELPERS <--
66
+
60
67
  format() {},
61
68
  // --> EVENT LISTENERS <--
69
+
62
70
  onSave() {}
63
71
  }
64
72
  });
@@ -80,6 +88,7 @@ ruleTester.run("vue-methods-separator", rule, {
80
88
  export default {
81
89
  methods: {
82
90
  // --> ACTIONS <--
91
+
83
92
  save() {
84
93
  // --> EVENT LISTENERS <--
85
94
  const status = "saved";
@@ -87,6 +96,7 @@ ruleTester.run("vue-methods-separator", rule, {
87
96
  return status;
88
97
  },
89
98
  // --> HELPERS <--
99
+
90
100
  format() {}
91
101
  }
92
102
  };
@@ -100,10 +110,13 @@ ruleTester.run("vue-methods-separator", rule, {
100
110
  export default defineComponent({
101
111
  methods: {
102
112
  // --> ACTIONS <--
113
+
103
114
  save() {},
104
115
  // --> HELPERS <--
116
+
105
117
  format() {},
106
118
  // --> EVENT LISTENERS <--
119
+
107
120
  onSave() {}
108
121
  }
109
122
  });
@@ -121,11 +134,42 @@ ruleTester.run("vue-methods-separator", rule, {
121
134
  }
122
135
  ]
123
136
  },
137
+ {
138
+ code: `
139
+ export default {
140
+ methods: {
141
+ // --> ACTIONS <--
142
+ /**
143
+ * Saves content
144
+ */
145
+ save() {}
146
+ }
147
+ };
148
+ `,
149
+ output: `
150
+ export default {
151
+ methods: {
152
+ // --> ACTIONS <--
153
+
154
+ /**
155
+ * Saves content
156
+ */
157
+ save() {}
158
+ }
159
+ };
160
+ `,
161
+ errors: [
162
+ {
163
+ messageId: "missingSeparatorSpacing"
164
+ }
165
+ ]
166
+ },
124
167
  {
125
168
  code: `
126
169
  export default {
127
170
  methods: {
128
171
  // --> HELPERS <--
172
+
129
173
  help() {},
130
174
  // --> OTHER <--
131
175
  run() {}
@@ -138,13 +182,33 @@ ruleTester.run("vue-methods-separator", rule, {
138
182
  }
139
183
  ]
140
184
  },
185
+ {
186
+ code: `
187
+ export default {
188
+ methods: {
189
+ // --> ACTIONS <--
190
+
191
+ save() {},
192
+ /* --> OTHER <-- */
193
+ run() {}
194
+ }
195
+ };
196
+ `,
197
+ errors: [
198
+ {
199
+ messageId: "invalidSeparator"
200
+ }
201
+ ]
202
+ },
141
203
  {
142
204
  code: `
143
205
  export default {
144
206
  methods: {
145
207
  // --> HELPERS <--
208
+
146
209
  format() {},
147
210
  // --> ACTIONS <--
211
+
148
212
  save() {}
149
213
  }
150
214
  };
@@ -153,8 +217,10 @@ ruleTester.run("vue-methods-separator", rule, {
153
217
  export default {
154
218
  methods: {
155
219
  // --> ACTIONS <--
220
+
156
221
  save() {},
157
222
  // --> HELPERS <--
223
+
158
224
  format() {}
159
225
  }
160
226
  };
@@ -242,8 +308,10 @@ ruleTester.run("vue-methods-separator", rule, {
242
308
  export default {
243
309
  methods: {
244
310
  // --> ACTIONS <--
311
+
245
312
  save() {},
246
313
  // --> ACTIONS <--
314
+
247
315
  publish() {}
248
316
  }
249
317
  };
@@ -260,8 +328,10 @@ ruleTester.run("vue-methods-separator", rule, {
260
328
  export default defineComponent({
261
329
  methods: {
262
330
  // --> EVENT LISTENERS <--
331
+
263
332
  onSave() {},
264
333
  // --> HELPERS <--
334
+
265
335
  format() {}
266
336
  }
267
337
  });
@@ -270,8 +340,10 @@ ruleTester.run("vue-methods-separator", rule, {
270
340
  export default defineComponent({
271
341
  methods: {
272
342
  // --> HELPERS <--
343
+
273
344
  format() {},
274
345
  // --> EVENT LISTENERS <--
346
+
275
347
  onSave() {}
276
348
  }
277
349
  });
@@ -305,8 +377,10 @@ ruleTester.run("vue-methods-separator", rule, {
305
377
  export default defineComponent({
306
378
  methods: {
307
379
  // --> EVENT LISTENERS <--
380
+
308
381
  onSave() {},
309
382
  // --> ACTIONS <--
383
+
310
384
  save() {}
311
385
  }
312
386
  });
@@ -317,8 +391,10 @@ ruleTester.run("vue-methods-separator", rule, {
317
391
  export default defineComponent({
318
392
  methods: {
319
393
  // --> ACTIONS <--
394
+
320
395
  save() {},
321
396
  // --> EVENT LISTENERS <--
397
+
322
398
  onSave() {}
323
399
  }
324
400
  });