@arcgis/eslint-config 5.2.0-next.2 → 5.2.0-next.21
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/dist/config/applications.d.ts +4 -3
- package/dist/config/applications.js +4 -4
- package/dist/config/extra.d.ts +4 -24
- package/dist/config/extra.js +2 -2
- package/dist/config/index.d.ts +3 -2
- package/dist/config/index.js +7 -10
- package/dist/config/lumina.d.ts +4 -3
- package/dist/config/lumina.js +8 -8
- package/dist/{estree-DW92hBTd.js → estree-CDc-die8.js} +1 -39
- package/dist/makePlugin-GmKey29v.js +41 -0
- package/dist/plugins/core/index.d.ts +4 -0
- package/dist/plugins/core/index.js +702 -0
- package/dist/plugins/lumina/index.d.ts +4 -5
- package/dist/plugins/lumina/index.js +2 -1
- package/dist/plugins/utils/makePlugin.d.ts +15 -16
- package/dist/plugins/webgis/index.d.ts +4 -5
- package/dist/plugins/webgis/index.js +794 -119
- package/dist/utils/defineBoundaries.d.ts +22 -38
- package/dist/utils/disableRules.d.ts +6 -6
- package/package.json +4 -2
- package/dist/config/storybook.d.ts +0 -2
- package/dist/plugins/lumina/plugin.d.ts +0 -8
- package/dist/plugins/lumina/rules/add-missing-jsx-import.d.ts +0 -2
- package/dist/plugins/lumina/rules/auto-add-type.d.ts +0 -3
- package/dist/plugins/lumina/rules/ban-events.d.ts +0 -6
- package/dist/plugins/lumina/rules/component-placement-rules.d.ts +0 -2
- package/dist/plugins/lumina/rules/consistent-event-naming.d.ts +0 -15
- package/dist/plugins/lumina/rules/consistent-nullability.d.ts +0 -2
- package/dist/plugins/lumina/rules/decorators-context.d.ts +0 -2
- package/dist/plugins/lumina/rules/explicit-setter-type.d.ts +0 -19
- package/dist/plugins/lumina/rules/member-ordering/build.d.ts +0 -4
- package/dist/plugins/lumina/rules/member-ordering/comments.d.ts +0 -19
- package/dist/plugins/lumina/rules/member-ordering/config.d.ts +0 -36
- package/dist/plugins/lumina/rules/member-ordering/normalize.d.ts +0 -10
- package/dist/plugins/lumina/rules/member-ordering.d.ts +0 -2
- package/dist/plugins/lumina/rules/no-create-element-component.d.ts +0 -2
- package/dist/plugins/lumina/rules/no-ignore-jsdoc-tag.d.ts +0 -2
- package/dist/plugins/lumina/rules/no-incorrect-dynamic-tag-name.d.ts +0 -3
- package/dist/plugins/lumina/rules/no-inline-arrow-in-ref.d.ts +0 -2
- package/dist/plugins/lumina/rules/no-inline-exposure-jsdoc-tag.d.ts +0 -2
- package/dist/plugins/lumina/rules/no-invalid-directives-prop.d.ts +0 -2
- package/dist/plugins/lumina/rules/no-jsdoc-xref-links.d.ts +0 -2
- package/dist/plugins/lumina/rules/no-jsx-spread.d.ts +0 -2
- package/dist/plugins/lumina/rules/no-listen-in-connected-callback.d.ts +0 -2
- package/dist/plugins/lumina/rules/no-non-component-exports.d.ts +0 -2
- package/dist/plugins/lumina/rules/no-property-name-start-with-on.d.ts +0 -2
- package/dist/plugins/lumina/rules/no-render-false.d.ts +0 -3
- package/dist/plugins/lumina/rules/no-unnecessary-assertion-on-event.d.ts +0 -3
- package/dist/plugins/lumina/rules/no-unnecessary-attribute-name.d.ts +0 -2
- package/dist/plugins/lumina/rules/no-unnecessary-bind-this.d.ts +0 -2
- package/dist/plugins/lumina/rules/no-unnecessary-key.d.ts +0 -2
- package/dist/plugins/lumina/rules/tag-name-rules.d.ts +0 -8
- package/dist/plugins/lumina/utils/checker.d.ts +0 -4
- package/dist/plugins/lumina/utils/estree.d.ts +0 -33
- package/dist/plugins/lumina/utils/tags.d.ts +0 -14
- package/dist/plugins/utils/helpers.d.ts +0 -2
- package/dist/plugins/webgis/plugin.d.ts +0 -8
- package/dist/plugins/webgis/rules/consistent-logging.d.ts +0 -2
- package/dist/plugins/webgis/rules/no-dts-files.d.ts +0 -2
- package/dist/plugins/webgis/rules/no-import-outside-src.d.ts +0 -2
- package/dist/plugins/webgis/rules/no-story-render-args-type-annotation.d.ts +0 -2
- package/dist/plugins/webgis/rules/no-touching-jsdoc.d.ts +0 -2
- package/dist/plugins/webgis/rules/no-unsafe-hash-links.d.ts +0 -2
- package/dist/plugins/webgis/rules/require-js-in-imports.d.ts +0 -2
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import { m as makeEslintPlugin } from "../../makePlugin-GmKey29v.js";
|
|
2
|
+
import { AST_NODE_TYPES, AST_TOKEN_TYPES } from "@typescript-eslint/utils";
|
|
3
|
+
import { generateDifferences, showInvisibles } from "prettier-linter-helpers";
|
|
4
|
+
import path, { relative, win32, posix, normalize } from "path";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
const plugin = makeEslintPlugin(
|
|
7
|
+
"core",
|
|
8
|
+
(rule) => `https://devtopia.esri.com/WebGIS/arcgis-web-components/tree/main/packages/support-packages/eslint-config/src/plugins/core/rules/${rule}.ts`
|
|
9
|
+
);
|
|
10
|
+
plugin.createRule({
|
|
11
|
+
name: "glsl-whitespace",
|
|
12
|
+
meta: {
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Checks that glsl tagged template strings don't have extra whitespace.",
|
|
15
|
+
defaultLevel: "error"
|
|
16
|
+
},
|
|
17
|
+
messages: {
|
|
18
|
+
delete: "Delete `{{ deleteText }}`",
|
|
19
|
+
unexpectedDiff: "Unexpected diff operation while processing GLSL whitespace."
|
|
20
|
+
},
|
|
21
|
+
schema: [],
|
|
22
|
+
type: "layout",
|
|
23
|
+
fixable: "code"
|
|
24
|
+
},
|
|
25
|
+
defaultOptions: [],
|
|
26
|
+
create(context) {
|
|
27
|
+
if (!context.filename.endsWith(".glsl.ts")) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
"TaggedTemplateExpression[tag.name='glsl']"(node) {
|
|
32
|
+
const quasis = node.quasi.quasis;
|
|
33
|
+
if (quasis.length > 1) {
|
|
34
|
+
processFixes(
|
|
35
|
+
context,
|
|
36
|
+
quasis[0],
|
|
37
|
+
(text) => removeLeadingBlankLines(removeMultipleBlankLines(removeTrailingWhitespace(text)))
|
|
38
|
+
);
|
|
39
|
+
for (let i = 1; i < quasis.length - 1; i++) {
|
|
40
|
+
processFixes(context, quasis[i], (text) => removeMultipleBlankLines(removeTrailingWhitespace(text)));
|
|
41
|
+
}
|
|
42
|
+
processFixes(
|
|
43
|
+
context,
|
|
44
|
+
quasis[quasis.length - 1],
|
|
45
|
+
(text) => removeTrailingBlankLines(removeMultipleBlankLines(removeTrailingWhitespace(text)))
|
|
46
|
+
);
|
|
47
|
+
} else {
|
|
48
|
+
processFixes(
|
|
49
|
+
context,
|
|
50
|
+
quasis[0],
|
|
51
|
+
(text) => removeTrailingBlankLines(removeLeadingBlankLines(removeMultipleBlankLines(removeTrailingWhitespace(text))))
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
function processFixes(context, node, fix) {
|
|
59
|
+
const sourceCode = context.sourceCode;
|
|
60
|
+
const text = sourceCode.getText(node);
|
|
61
|
+
const fixed = fix(text);
|
|
62
|
+
if (text === fixed) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const diffs = generateDifferences(text, fixed);
|
|
66
|
+
for (const diff of diffs) {
|
|
67
|
+
const { operation, offset } = diff;
|
|
68
|
+
const deleteText = "deleteText" in diff ? diff.deleteText : "";
|
|
69
|
+
const absOffset = node.range[0] + offset;
|
|
70
|
+
const range = [absOffset, absOffset + deleteText.length];
|
|
71
|
+
const [start, end] = range.map((index) => sourceCode.getLocFromIndex(index));
|
|
72
|
+
if (operation === generateDifferences.DELETE) {
|
|
73
|
+
context.report({
|
|
74
|
+
loc: { start, end },
|
|
75
|
+
messageId: "delete",
|
|
76
|
+
data: { deleteText: showInvisibles(deleteText) },
|
|
77
|
+
fix: (fixer) => fixer.removeRange(range)
|
|
78
|
+
});
|
|
79
|
+
} else {
|
|
80
|
+
context.report({ loc: { start, end }, messageId: "unexpectedDiff" });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function removeTrailingWhitespace(text) {
|
|
85
|
+
return text.replace(/[\t ]+(\r?\n)/gu, "$1");
|
|
86
|
+
}
|
|
87
|
+
function removeMultipleBlankLines(text) {
|
|
88
|
+
return text.replace(/(\r?\n\r?\n)(\r?\n)*/gu, "$1");
|
|
89
|
+
}
|
|
90
|
+
function removeLeadingBlankLines(text) {
|
|
91
|
+
return text.replace(/`(\r?\n)(\r?\n)*/gu, "`$1");
|
|
92
|
+
}
|
|
93
|
+
function removeTrailingBlankLines(text) {
|
|
94
|
+
return text.replace(/(\r?\n)(\r?\n)*([ \t]*)`$/gu, "$1$3`");
|
|
95
|
+
}
|
|
96
|
+
function findProjectRoot(filename) {
|
|
97
|
+
const normalized = filename.replaceAll("\\", "/");
|
|
98
|
+
let minimumIndex = Number.POSITIVE_INFINITY;
|
|
99
|
+
for (const marker of ["src/", "tests/", "test-apps/", "esri/"]) {
|
|
100
|
+
const index = normalized.indexOf(marker);
|
|
101
|
+
if (index === 0) {
|
|
102
|
+
return ".";
|
|
103
|
+
} else if (index > 0 && normalized.charCodeAt(index - 1) === slashCharCode) {
|
|
104
|
+
minimumIndex = Math.min(minimumIndex, index);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return minimumIndex === Number.POSITIVE_INFINITY ? void 0 : normalized.slice(0, minimumIndex);
|
|
108
|
+
}
|
|
109
|
+
const slashCharCode = 47;
|
|
110
|
+
const isWindows = process.platform === "win32";
|
|
111
|
+
const collator = new Intl.Collator("en-US");
|
|
112
|
+
plugin.createRule({
|
|
113
|
+
name: "imports-format",
|
|
114
|
+
meta: {
|
|
115
|
+
docs: {
|
|
116
|
+
description: "Checks that imports are sorted and grouped",
|
|
117
|
+
defaultLevel: "error"
|
|
118
|
+
},
|
|
119
|
+
messages: {
|
|
120
|
+
importFormat: "Imports are incorrectly sorted and/or grouped",
|
|
121
|
+
missingProjectRoot: "Unable to locate project root for this file."
|
|
122
|
+
},
|
|
123
|
+
schema: [],
|
|
124
|
+
type: "layout",
|
|
125
|
+
fixable: "code"
|
|
126
|
+
},
|
|
127
|
+
defaultOptions: [],
|
|
128
|
+
create(context) {
|
|
129
|
+
const filePath = context.filename;
|
|
130
|
+
const sourceCode = context.sourceCode;
|
|
131
|
+
const basePath = findProjectRoot(filePath);
|
|
132
|
+
if (!basePath) {
|
|
133
|
+
return {
|
|
134
|
+
Program(node) {
|
|
135
|
+
context.report({ node, messageId: "missingProjectRoot" });
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const absImportPath = path.dirname(relative(basePath, filePath));
|
|
140
|
+
const newLine = detectLineEnding(sourceCode);
|
|
141
|
+
const allStatements = [];
|
|
142
|
+
return {
|
|
143
|
+
"Program > :statement"(node) {
|
|
144
|
+
allStatements.push(node);
|
|
145
|
+
},
|
|
146
|
+
"Program:exit"() {
|
|
147
|
+
const importStatements = allStatements.filter(isImportDeclaration);
|
|
148
|
+
if (importStatements.length === 0) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const firstImport = importStatements[0];
|
|
152
|
+
const lastImport = importStatements[importStatements.length - 1];
|
|
153
|
+
const firstIndex = allStatements.indexOf(firstImport);
|
|
154
|
+
const lastIndex = allStatements.indexOf(lastImport);
|
|
155
|
+
const statements = allStatements.slice(firstIndex, lastIndex + 1);
|
|
156
|
+
const formatted = formatImports(sourceCode, statements, absImportPath, basePath, newLine);
|
|
157
|
+
const start = getAttachedCommentStart(sourceCode, firstImport);
|
|
158
|
+
const end = lastImport.range[1];
|
|
159
|
+
const original = sourceCode.text.slice(start, end);
|
|
160
|
+
const originalNormalized = normalizeLineEndings(original);
|
|
161
|
+
if (originalNormalized.trim() !== normalizeLineEndings(formatted)) {
|
|
162
|
+
const reducedFixRange = reduceFixRange(original, formatted);
|
|
163
|
+
const absRange = {
|
|
164
|
+
start: start + reducedFixRange.start,
|
|
165
|
+
end: start + reducedFixRange.end
|
|
166
|
+
};
|
|
167
|
+
context.report({
|
|
168
|
+
loc: { start: sourceCode.getLocFromIndex(absRange.start), end: sourceCode.getLocFromIndex(absRange.end) },
|
|
169
|
+
messageId: "importFormat",
|
|
170
|
+
fix: (fixer) => fixer.replaceTextRange([absRange.start, absRange.end], reducedFixRange.replacement)
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
function detectLineEnding(sourceCode) {
|
|
178
|
+
const hasWindowsLineEndings = sourceCode.text.includes("\r\n");
|
|
179
|
+
const hasSaneLineEndings = sourceCode.text.includes("\n");
|
|
180
|
+
if (hasWindowsLineEndings || !hasSaneLineEndings && isWindows) {
|
|
181
|
+
return "\r\n";
|
|
182
|
+
} else {
|
|
183
|
+
return "\n";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function isImportDeclaration(statement) {
|
|
187
|
+
return statement.type === AST_NODE_TYPES.ImportDeclaration;
|
|
188
|
+
}
|
|
189
|
+
function formatImports(sourceCode, statements, absImportPath, basePath, newLine) {
|
|
190
|
+
const imports = [];
|
|
191
|
+
const others = [];
|
|
192
|
+
for (const stmt of statements) {
|
|
193
|
+
if (isImportDeclaration(stmt)) {
|
|
194
|
+
imports.push(stmt);
|
|
195
|
+
} else {
|
|
196
|
+
others.push(stmt);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const grouped = groupImportsByPath(imports, absImportPath, basePath);
|
|
200
|
+
const importBlocks = grouped.map((groupedImports) => formatGroupedImports(sourceCode, groupedImports, newLine));
|
|
201
|
+
const otherBlocks = others.map((other) => sourceCode.getText(other));
|
|
202
|
+
const blocks = importBlocks.concat(otherBlocks);
|
|
203
|
+
return blocks.join(newLine + newLine);
|
|
204
|
+
}
|
|
205
|
+
function groupImportsByPath(imports, absImportPath, basePath) {
|
|
206
|
+
const perPath = {};
|
|
207
|
+
for (const importStatement of imports) {
|
|
208
|
+
const importPath = importStatementPathName(importStatement, absImportPath, basePath);
|
|
209
|
+
let groupedImports = perPath[importPath];
|
|
210
|
+
if (!groupedImports) {
|
|
211
|
+
groupedImports = { path: importPath, imports: [] };
|
|
212
|
+
perPath[importPath] = groupedImports;
|
|
213
|
+
}
|
|
214
|
+
groupedImports.imports.push(importStatement);
|
|
215
|
+
}
|
|
216
|
+
const groups = Object.keys(perPath);
|
|
217
|
+
groups.sort(collator.compare);
|
|
218
|
+
return groups.map((groupName) => {
|
|
219
|
+
const group = perPath[groupName];
|
|
220
|
+
group.imports.sort((a, b) => {
|
|
221
|
+
const pa = importStatementPathString(a);
|
|
222
|
+
const pb = importStatementPathString(b);
|
|
223
|
+
return collator.compare(pa, pb);
|
|
224
|
+
});
|
|
225
|
+
return group;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function importStatementPathName(importStatement, absImportPath, basePath) {
|
|
229
|
+
return importPathName(importStatementPathString(importStatement), absImportPath, basePath);
|
|
230
|
+
}
|
|
231
|
+
function importStatementPathString(importStatement) {
|
|
232
|
+
return importStatement.source.value;
|
|
233
|
+
}
|
|
234
|
+
function importPathName(importPath, absImportPath, basePath) {
|
|
235
|
+
importPath = importPath.replace(/!.*/gu, "");
|
|
236
|
+
if (!importPath.startsWith(".")) {
|
|
237
|
+
const importDir = path.dirname(importPath);
|
|
238
|
+
if (importDir === ".") {
|
|
239
|
+
return importPath;
|
|
240
|
+
}
|
|
241
|
+
return importDir;
|
|
242
|
+
}
|
|
243
|
+
importPath = path.join(absImportPath, importPath);
|
|
244
|
+
if (!path.isAbsolute(importPath)) {
|
|
245
|
+
importPath = path.join(basePath, importPath);
|
|
246
|
+
}
|
|
247
|
+
importPath = path.resolve(path.normalize(importPath));
|
|
248
|
+
return path.dirname(relative(basePath, importPath));
|
|
249
|
+
}
|
|
250
|
+
function formatGroupedImports(sourceCode, groupedImports, newLine) {
|
|
251
|
+
const importHeader = `// ${groupedImports.path.replace(/[\\/]/gu, ".")}`;
|
|
252
|
+
const imports = groupedImports.imports.map((importDeclaration) => sourceCode.getText(importDeclaration).trim()).join(newLine).trim();
|
|
253
|
+
return `${importHeader}${newLine}${imports}`;
|
|
254
|
+
}
|
|
255
|
+
function getAttachedCommentStart(sourceCode, statement) {
|
|
256
|
+
const comments = sourceCode.getCommentsBefore(statement);
|
|
257
|
+
let start = statement.range[0];
|
|
258
|
+
if (comments.length > 0) {
|
|
259
|
+
const comment = comments[comments.length - 1];
|
|
260
|
+
if (comment.type === AST_TOKEN_TYPES.Line && comment.loc.start.line === statement.loc.start.line - 1) {
|
|
261
|
+
start = comment.range[0];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return start;
|
|
265
|
+
}
|
|
266
|
+
function normalizeLineEndings(s) {
|
|
267
|
+
return s.replace(/\r\n/gu, "\n");
|
|
268
|
+
}
|
|
269
|
+
function reduceFixRange(original, formatted) {
|
|
270
|
+
let originalPos = 0;
|
|
271
|
+
let formattedPos = 0;
|
|
272
|
+
let originalStart = 0;
|
|
273
|
+
let formattedStart = 0;
|
|
274
|
+
const isWhitespace = (s) => s === " " || s === "\n" || s === "\r" || s === " ";
|
|
275
|
+
while (isWhitespace(original[originalPos])) {
|
|
276
|
+
++originalPos;
|
|
277
|
+
}
|
|
278
|
+
while (true) {
|
|
279
|
+
if (original[originalPos] === "\r") {
|
|
280
|
+
++originalPos;
|
|
281
|
+
}
|
|
282
|
+
if (formatted[formattedPos] === "\r") {
|
|
283
|
+
++formattedPos;
|
|
284
|
+
}
|
|
285
|
+
if (original[originalPos] !== formatted[originalPos]) {
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
if (original[originalPos] === "\n") {
|
|
289
|
+
originalStart = originalPos + 1;
|
|
290
|
+
formattedStart = formattedPos + 1;
|
|
291
|
+
}
|
|
292
|
+
++originalPos;
|
|
293
|
+
++formattedPos;
|
|
294
|
+
}
|
|
295
|
+
originalPos = original.length - 1;
|
|
296
|
+
formattedPos = formatted.length - 1;
|
|
297
|
+
let originalEnd = originalPos;
|
|
298
|
+
let formattedEnd = formattedPos;
|
|
299
|
+
while (isWhitespace(original[originalPos])) {
|
|
300
|
+
--originalPos;
|
|
301
|
+
}
|
|
302
|
+
while (true) {
|
|
303
|
+
if (original[originalPos] === "\r") {
|
|
304
|
+
--originalPos;
|
|
305
|
+
}
|
|
306
|
+
if (formatted[formattedPos] === "\r") {
|
|
307
|
+
--formattedPos;
|
|
308
|
+
}
|
|
309
|
+
if (original[originalPos] === "\n") {
|
|
310
|
+
originalEnd = originalPos;
|
|
311
|
+
formattedEnd = formattedPos;
|
|
312
|
+
}
|
|
313
|
+
if (original[originalPos] !== formatted[formattedPos] || originalPos === originalStart || formattedPos === formattedStart) {
|
|
314
|
+
return {
|
|
315
|
+
start: originalStart,
|
|
316
|
+
end: originalEnd + 1,
|
|
317
|
+
replacement: formatted.slice(formattedStart, formattedEnd + 1)
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
--originalPos;
|
|
321
|
+
--formattedPos;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
plugin.createRule({
|
|
325
|
+
name: "no-duplicate-import-comments",
|
|
326
|
+
meta: {
|
|
327
|
+
docs: {
|
|
328
|
+
description: "Checks for duplicate comments before the first import declaration.",
|
|
329
|
+
defaultLevel: "error"
|
|
330
|
+
},
|
|
331
|
+
messages: { duplicate: "Delete duplicate comment" },
|
|
332
|
+
schema: [],
|
|
333
|
+
type: "problem",
|
|
334
|
+
fixable: "code"
|
|
335
|
+
},
|
|
336
|
+
defaultOptions: [],
|
|
337
|
+
create(context) {
|
|
338
|
+
const sourceCode = context.sourceCode;
|
|
339
|
+
let firstImport;
|
|
340
|
+
return {
|
|
341
|
+
ImportDeclaration(node) {
|
|
342
|
+
if (firstImport) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
firstImport = node;
|
|
346
|
+
const comments = sourceCode.getCommentsBefore(firstImport);
|
|
347
|
+
if (comments.length > 1) {
|
|
348
|
+
let previousComment = comments[0];
|
|
349
|
+
for (let i = 1; i < comments.length; i++) {
|
|
350
|
+
const comment = comments[i];
|
|
351
|
+
if (previousComment.value === comment.value) {
|
|
352
|
+
context.report({
|
|
353
|
+
node: previousComment,
|
|
354
|
+
messageId: "duplicate",
|
|
355
|
+
fix: (fixer) => fixer.remove(previousComment)
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
previousComment = comment;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
const emptyTodoTagRe = /^[\s*]*@todo\s*$/mu;
|
|
366
|
+
plugin.createRule({
|
|
367
|
+
name: "no-empty-jsdoc-todo",
|
|
368
|
+
meta: {
|
|
369
|
+
docs: {
|
|
370
|
+
description: "Checks for `todo` JSDoc tags with missing descriptions.",
|
|
371
|
+
defaultLevel: "error"
|
|
372
|
+
},
|
|
373
|
+
messages: { empty: "JSDoc `todo` tag description required" },
|
|
374
|
+
schema: [],
|
|
375
|
+
type: "problem"
|
|
376
|
+
},
|
|
377
|
+
defaultOptions: [],
|
|
378
|
+
create(context) {
|
|
379
|
+
const allComments = context.sourceCode.getAllComments();
|
|
380
|
+
return {
|
|
381
|
+
Program() {
|
|
382
|
+
for (const comment of allComments) {
|
|
383
|
+
if (comment.type === AST_TOKEN_TYPES.Block && comment.value.startsWith("*") && emptyTodoTagRe.test(comment.value)) {
|
|
384
|
+
context.report({ loc: comment.loc, messageId: "empty" });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
plugin.createRule({
|
|
392
|
+
name: "no-invalid-declared-class",
|
|
393
|
+
meta: {
|
|
394
|
+
docs: {
|
|
395
|
+
description: "Enforces the declared class name passed to the @subclass() decorator",
|
|
396
|
+
defaultLevel: "error"
|
|
397
|
+
},
|
|
398
|
+
messages: {
|
|
399
|
+
invalidPath: "Invalid path passed to @subclass(). It should be a string literal matching the file path, without the extension and with path separators replaced with `.`"
|
|
400
|
+
},
|
|
401
|
+
fixable: "code",
|
|
402
|
+
schema: [],
|
|
403
|
+
type: "problem"
|
|
404
|
+
},
|
|
405
|
+
defaultOptions: [],
|
|
406
|
+
create(context) {
|
|
407
|
+
const entries = /* @__PURE__ */ new Set();
|
|
408
|
+
return {
|
|
409
|
+
"ClassDeclaration"(node) {
|
|
410
|
+
for (const { expression } of node.decorators) {
|
|
411
|
+
if (expression.type !== AST_NODE_TYPES.CallExpression) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const callee = expression.callee;
|
|
415
|
+
if (callee.type !== AST_NODE_TYPES.Identifier || callee.name !== "subclass") {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const arg = expression.arguments.at(0);
|
|
419
|
+
if (arg == null) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (arg.type !== AST_NODE_TYPES.Literal || typeof arg.value !== "string") {
|
|
423
|
+
context.report({ node: arg, messageId: "invalidPath" });
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
entries.add({ arg, className: node.id?.name ?? "" });
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
"Program:exit"() {
|
|
430
|
+
const hasMultipleClasses = entries.size > 1;
|
|
431
|
+
for (const { arg, className } of entries) {
|
|
432
|
+
const relativePath = getDeclaredClass(context.filename);
|
|
433
|
+
const actual = arg.value;
|
|
434
|
+
let expected = relativePath;
|
|
435
|
+
if (hasMultipleClasses && !relativePath?.endsWith(`.${className}`)) {
|
|
436
|
+
expected = `${relativePath}.${className}`;
|
|
437
|
+
}
|
|
438
|
+
if (expected != null && actual !== expected) {
|
|
439
|
+
context.report({
|
|
440
|
+
node: arg,
|
|
441
|
+
messageId: "invalidPath",
|
|
442
|
+
fix: (fixer) => fixer.replaceText(arg, `"${expected}"`)
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
function getDeclaredClass(filename) {
|
|
451
|
+
const root = findProjectRoot(filename);
|
|
452
|
+
if (!root) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const relativePath = relative(root, filename).replaceAll(win32.sep, ".").replaceAll(posix.sep, ".").replace(/\.[^/.]+$/u, "");
|
|
456
|
+
return relativePath;
|
|
457
|
+
}
|
|
458
|
+
const tsRe = /\.tsx?$/u;
|
|
459
|
+
let gitIndexFiles;
|
|
460
|
+
function getGitIndexFiles(cwd) {
|
|
461
|
+
if (gitIndexFiles) {
|
|
462
|
+
return gitIndexFiles;
|
|
463
|
+
}
|
|
464
|
+
gitIndexFiles = /* @__PURE__ */ new Set();
|
|
465
|
+
const gitStdOut = execSync("git ls-files", { cwd, encoding: "utf-8" });
|
|
466
|
+
for (let file of gitStdOut.split("\n")) {
|
|
467
|
+
file = file.trim();
|
|
468
|
+
if (file.endsWith(".js") || file.endsWith(".d.ts")) {
|
|
469
|
+
gitIndexFiles.add(normalize(file));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return gitIndexFiles;
|
|
473
|
+
}
|
|
474
|
+
plugin.createRule({
|
|
475
|
+
name: "no-redundant-checked-in-files",
|
|
476
|
+
meta: {
|
|
477
|
+
docs: {
|
|
478
|
+
description: "Checks that files redundant to TypeScript implementation files are not checked in.",
|
|
479
|
+
defaultLevel: "error"
|
|
480
|
+
},
|
|
481
|
+
messages: {
|
|
482
|
+
js: "Redundant JavaScript file detected. Please `git rm -f {{file}}`",
|
|
483
|
+
dts: "Redundant declaration file detected. Please `git rm -f {{file}}`",
|
|
484
|
+
missingProjectRoot: "Unable to locate project root for this file."
|
|
485
|
+
},
|
|
486
|
+
schema: [],
|
|
487
|
+
type: "problem"
|
|
488
|
+
},
|
|
489
|
+
defaultOptions: [],
|
|
490
|
+
create(context) {
|
|
491
|
+
let filePath = context.filename;
|
|
492
|
+
return {
|
|
493
|
+
Program(node) {
|
|
494
|
+
if (!filePath.endsWith(".d.ts") && tsRe.test(filePath)) {
|
|
495
|
+
const projectRoot = findProjectRoot(filePath);
|
|
496
|
+
if (!projectRoot) {
|
|
497
|
+
context.report({ node, messageId: "missingProjectRoot" });
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const gitIndexFiles2 = getGitIndexFiles(projectRoot);
|
|
501
|
+
if (!gitIndexFiles2) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
filePath = relative(projectRoot, filePath);
|
|
505
|
+
const jsFilename = filePath.replace(tsRe, ".js");
|
|
506
|
+
if (gitIndexFiles2.has(jsFilename)) {
|
|
507
|
+
context.report({ node, messageId: "js", data: { file: jsFilename } });
|
|
508
|
+
}
|
|
509
|
+
const dtsFilename = filePath.replace(tsRe, ".d.ts");
|
|
510
|
+
if (gitIndexFiles2.has(dtsFilename)) {
|
|
511
|
+
context.report({ node, messageId: "dts", data: { file: dtsFilename } });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
plugin.createRule({
|
|
519
|
+
name: "prefer-relative-imports",
|
|
520
|
+
meta: {
|
|
521
|
+
docs: {
|
|
522
|
+
description: "Checks that whenever possible import and export paths are relative instead of absolute.",
|
|
523
|
+
defaultLevel: "error"
|
|
524
|
+
},
|
|
525
|
+
messages: {
|
|
526
|
+
importFormat: 'Prefer relative ("{{relPath}}") over absolute path',
|
|
527
|
+
missingProjectRoot: "Unable to locate project root for this file."
|
|
528
|
+
},
|
|
529
|
+
schema: [],
|
|
530
|
+
type: "problem",
|
|
531
|
+
fixable: "code"
|
|
532
|
+
},
|
|
533
|
+
defaultOptions: [],
|
|
534
|
+
create(context) {
|
|
535
|
+
const filePath = context.filename;
|
|
536
|
+
const basePath = findProjectRoot(filePath);
|
|
537
|
+
if (!basePath) {
|
|
538
|
+
return {
|
|
539
|
+
Program(node) {
|
|
540
|
+
context.report({ node, messageId: "missingProjectRoot" });
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
const absImportPath = path.dirname(relative(basePath, filePath));
|
|
545
|
+
const importRoot = pathRoot(absImportPath);
|
|
546
|
+
return {
|
|
547
|
+
"ExportAllDeclaration[source.type='Literal']"(node) {
|
|
548
|
+
verifyRelativeImport(context, node.source, absImportPath, basePath, importRoot);
|
|
549
|
+
},
|
|
550
|
+
"ExportNamedDeclaration[source.type='Literal']"(node) {
|
|
551
|
+
verifyRelativeImport(context, node.source, absImportPath, basePath, importRoot);
|
|
552
|
+
},
|
|
553
|
+
"ImportDeclaration"(node) {
|
|
554
|
+
verifyRelativeImport(context, node.source, absImportPath, basePath, importRoot);
|
|
555
|
+
},
|
|
556
|
+
"ImportExpression[source.type='Literal']"(node) {
|
|
557
|
+
if (node.source.type === AST_NODE_TYPES.Literal && typeof node.source.value === "string") {
|
|
558
|
+
verifyRelativeImport(context, node.source, absImportPath, basePath, importRoot);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
function pathRoot(p) {
|
|
565
|
+
const dirName = path.dirname(p);
|
|
566
|
+
return dirName === "." ? p : pathRoot(dirName);
|
|
567
|
+
}
|
|
568
|
+
function verifyRelativeImport(context, node, absImportPath, basePath, importRoot) {
|
|
569
|
+
const modulePath = node.value;
|
|
570
|
+
if (modulePath.startsWith(".")) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (pathRoot(modulePath) === importRoot) {
|
|
574
|
+
let relPath = path.relative(path.join(basePath, absImportPath), path.join(basePath, modulePath));
|
|
575
|
+
if (!relPath.startsWith(".")) {
|
|
576
|
+
relPath = `./${relPath}`;
|
|
577
|
+
}
|
|
578
|
+
if (isWindows) {
|
|
579
|
+
relPath = relPath.replaceAll(win32.sep, posix.sep);
|
|
580
|
+
}
|
|
581
|
+
context.report({
|
|
582
|
+
node,
|
|
583
|
+
messageId: "importFormat",
|
|
584
|
+
data: { relPath },
|
|
585
|
+
fix: (fixer) => fixer.replaceTextRange([node.range[0] + 1, node.range[1] - 1], relPath)
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
plugin.createRule({
|
|
590
|
+
name: "require-calcite-import",
|
|
591
|
+
meta: {
|
|
592
|
+
docs: {
|
|
593
|
+
description: "Checks that the corresponding calcite component import exists for each calcite component tag used. It also checks that there are no extra unnecessary imports.",
|
|
594
|
+
defaultLevel: "error"
|
|
595
|
+
},
|
|
596
|
+
messages: {
|
|
597
|
+
missingImport: 'Import missing: import("@esri/calcite-components/dist/components/{{tag}}")',
|
|
598
|
+
needlessImport: "Import is unnecessary since no matching calcite component tag found"
|
|
599
|
+
},
|
|
600
|
+
schema: [],
|
|
601
|
+
type: "problem"
|
|
602
|
+
},
|
|
603
|
+
defaultOptions: [],
|
|
604
|
+
create(context) {
|
|
605
|
+
const tags = [];
|
|
606
|
+
const imports = [];
|
|
607
|
+
return {
|
|
608
|
+
"JSXOpeningElement[name.name=/^calcite-/]"(node) {
|
|
609
|
+
if (node.name.type === AST_NODE_TYPES.JSXIdentifier) {
|
|
610
|
+
const name = node.name.name;
|
|
611
|
+
tags.push([name, node]);
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
"ImportExpression[source.type='Literal']"(node) {
|
|
615
|
+
if (node.source.type === AST_NODE_TYPES.Literal && typeof node.source.value === "string") {
|
|
616
|
+
const importString = node.source.value;
|
|
617
|
+
if (importString.startsWith("@esri/calcite-components/")) {
|
|
618
|
+
const importModule = importString.slice(importString.lastIndexOf("/") + 1);
|
|
619
|
+
imports.push([importModule, node]);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
"Program:exit"() {
|
|
624
|
+
for (const [tag, node] of tags) {
|
|
625
|
+
if (!imports.some(([importModule]) => tag === importModule)) {
|
|
626
|
+
context.report({ node, messageId: "missingImport", data: { tag } });
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
for (const [importModule, node] of imports) {
|
|
630
|
+
if (!tags.some(([tag]) => importModule === tag)) {
|
|
631
|
+
context.report({ node, messageId: "needlessImport" });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
plugin.createRule({
|
|
639
|
+
name: "spec-suite-filename",
|
|
640
|
+
meta: {
|
|
641
|
+
docs: {
|
|
642
|
+
description: "Enforce filenames to be part of spec (test) top-level suites.",
|
|
643
|
+
defaultLevel: "error"
|
|
644
|
+
},
|
|
645
|
+
messages: {
|
|
646
|
+
name: "Suite name must be the name of the spec file",
|
|
647
|
+
duplicateSuite: "There can only be one top level suite",
|
|
648
|
+
unresolved: "Could not determine expected suite name for this spec file"
|
|
649
|
+
},
|
|
650
|
+
schema: [],
|
|
651
|
+
type: "problem",
|
|
652
|
+
fixable: "code"
|
|
653
|
+
},
|
|
654
|
+
defaultOptions: [],
|
|
655
|
+
create(context) {
|
|
656
|
+
const filename = context.filename;
|
|
657
|
+
if (!filename.endsWith(".spec.ts")) {
|
|
658
|
+
return {};
|
|
659
|
+
}
|
|
660
|
+
const toplevelSuiteName = getToplevelSuiteName(filename);
|
|
661
|
+
if (!toplevelSuiteName) {
|
|
662
|
+
return {
|
|
663
|
+
Program(node) {
|
|
664
|
+
context.report({ node, messageId: "unresolved" });
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
let numToplevel = 0;
|
|
669
|
+
return {
|
|
670
|
+
"Program > ExpressionStatement > CallExpression[callee.name='suite']"(node) {
|
|
671
|
+
++numToplevel;
|
|
672
|
+
if (numToplevel > 1) {
|
|
673
|
+
context.report({ node, messageId: "duplicateSuite" });
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const first = node.arguments[0];
|
|
677
|
+
if (!first) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (first.type !== AST_NODE_TYPES.Literal || typeof first.value !== "string" || first.value !== toplevelSuiteName) {
|
|
681
|
+
context.report({
|
|
682
|
+
node: first,
|
|
683
|
+
messageId: "name",
|
|
684
|
+
fix: (fixer) => fixer.replaceText(first, `"${toplevelSuiteName}"`)
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
function getToplevelSuiteName(filename) {
|
|
692
|
+
const root = findProjectRoot(filename);
|
|
693
|
+
if (!root) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const relativePath = relative(root, filename).replaceAll(win32.sep, posix.sep);
|
|
697
|
+
return relativePath.replace(/^tests\/(.*)\.spec\.ts/u, "$1");
|
|
698
|
+
}
|
|
699
|
+
const corePlugin = plugin.finalize();
|
|
700
|
+
export {
|
|
701
|
+
corePlugin
|
|
702
|
+
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
};
|
|
1
|
+
import type { EslintPlugin } from "../utils/makePlugin.js";
|
|
2
|
+
|
|
3
|
+
/** Plugin that provides rules that are widely applicable for Lumina projects. */
|
|
4
|
+
export const luminaPlugin: EslintPlugin;
|