@arcgis/eslint-config 4.33.0-next.61 → 4.33.0-next.62
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/extra.js +2 -3
- package/dist/config/index.d.ts +1 -18
- package/dist/config/index.js +4 -9
- package/dist/config/lumina.d.ts +1 -5
- package/dist/config/lumina.js +6 -13
- package/dist/config/storybook.d.ts +1 -1
- package/dist/{chunk-XL2W5WPA.js → makePlugin-dhgL5LDQ.js} +2 -6
- package/dist/plugins/lumina/index.js +1855 -5
- package/dist/plugins/lumina/rules/add-missing-jsx-import.d.ts +1 -1
- package/dist/plugins/lumina/rules/auto-add-type.d.ts +2 -2
- package/dist/plugins/lumina/rules/component-placement-rules.d.ts +1 -1
- package/dist/plugins/lumina/rules/consistent-event-naming.d.ts +1 -1
- package/dist/plugins/lumina/rules/decorators-context.d.ts +1 -1
- package/dist/plugins/lumina/rules/member-ordering/build.d.ts +3 -3
- package/dist/plugins/lumina/rules/member-ordering/comments.d.ts +2 -2
- package/dist/plugins/lumina/rules/member-ordering/config.d.ts +1 -1
- package/dist/plugins/lumina/rules/member-ordering/normalize.d.ts +2 -2
- package/dist/plugins/lumina/rules/member-ordering.d.ts +1 -1
- package/dist/plugins/lumina/rules/no-ignore-jsdoc-tag.d.ts +1 -1
- package/dist/plugins/lumina/rules/no-incorrect-dynamic-tag-name.d.ts +2 -2
- package/dist/plugins/lumina/rules/no-inline-arrow-in-ref.d.ts +1 -1
- package/dist/plugins/lumina/rules/no-invalid-directives-prop.d.ts +1 -1
- package/dist/plugins/lumina/rules/no-jsx-spread.d.ts +1 -1
- package/dist/plugins/lumina/rules/no-listen-in-connected-callback.d.ts +1 -1
- package/dist/plugins/lumina/rules/no-non-component-exports.d.ts +1 -1
- package/dist/plugins/lumina/rules/no-property-name-start-with-on.d.ts +1 -1
- package/dist/plugins/lumina/rules/no-render-false.d.ts +2 -2
- package/dist/plugins/lumina/rules/no-unnecessary-attribute-name.d.ts +1 -1
- package/dist/plugins/lumina/rules/no-unnecessary-bind-this.d.ts +1 -1
- package/dist/plugins/lumina/rules/no-unnecessary-key.d.ts +1 -1
- package/dist/plugins/lumina/rules/tag-name-rules.d.ts +1 -1
- package/dist/plugins/lumina/utils/checker.d.ts +1 -1
- package/dist/plugins/lumina/utils/creator.d.ts +2 -2
- package/dist/plugins/lumina/utils/estree.d.ts +1 -1
- package/dist/plugins/utils/makePlugin.d.ts +2 -9
- package/dist/plugins/utils/tests.d.ts +1 -1
- package/dist/plugins/webgis/index.js +142 -5
- package/dist/plugins/webgis/rules/no-import-outside-src.d.ts +1 -1
- package/dist/plugins/webgis/rules/no-touching-jsdoc.d.ts +1 -1
- package/dist/plugins/webgis/rules/require-js-in-core-import.d.ts +1 -1
- package/dist/plugins/webgis/utils/creator.d.ts +2 -2
- package/package.json +2 -2
- package/dist/chunk-2YLP4Q2L.js +0 -160
- package/dist/chunk-4ICYLGON.js +0 -1978
- package/dist/plugins/lumina/rules/add-missing-jsx-import.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/auto-add-type.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/component-placement-rules.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/consistent-event-naming.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/decorators-context.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/member-ordering.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/no-ignore-jsdoc-tag.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/no-incorrect-dynamic-tag-name.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/no-inline-arrow-in-ref.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/no-invalid-directives-prop.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/no-jsx-spread.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/no-listen-in-connected-callback.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/no-non-component-exports.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/no-property-name-start-with-on.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/no-render-false.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/no-unnecessary-attribute-name.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/no-unnecessary-bind-this.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/no-unnecessary-key.test.d.ts +0 -1
- package/dist/plugins/lumina/rules/tag-name-rules.spec.d.ts +0 -1
- package/dist/plugins/webgis/rules/no-import-outside-src.test.d.ts +0 -1
- package/dist/plugins/webgis/rules/no-touching-jsdoc.test.d.ts +0 -1
- package/dist/plugins/webgis/rules/require-js-in-core-import.test.d.ts +0 -1
|
@@ -1,7 +1,1857 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
} from "../../
|
|
4
|
-
import "
|
|
1
|
+
import { ESLintUtils, AST_NODE_TYPES, AST_TOKEN_TYPES } from "@typescript-eslint/utils";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { m as makeEslintPlugin } from "../../makePlugin-dhgL5LDQ.js";
|
|
4
|
+
import { camelToKebab } from "@arcgis/components-utils";
|
|
5
|
+
const createRule = ESLintUtils.RuleCreator(
|
|
6
|
+
(rule) => `https://devtopia.esri.com/WebGIS/arcgis-web-components/tree/main/packages/support-packages/eslint-config/src/plugins/lumina/rules/${rule}.ts`
|
|
7
|
+
);
|
|
8
|
+
const unwrapExpression = (expression) => expression.type === AST_NODE_TYPES.TSAsExpression || expression.type === AST_NODE_TYPES.TSNonNullExpression || expression.type === AST_NODE_TYPES.TSSatisfiesExpression ? unwrapExpression(expression.expression) : expression;
|
|
9
|
+
const luminaEntrypointName = "@arcgis/lumina";
|
|
10
|
+
const luminaTestEntrypointName = "@arcgis/lumina-compiler/testing";
|
|
11
|
+
const luminaJsxExportName = "h";
|
|
12
|
+
function checkForLuminaJsx() {
|
|
13
|
+
const ImportDeclaration = (node) => {
|
|
14
|
+
if (node.source.value !== luminaEntrypointName) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
for (const specifier of node.specifiers) {
|
|
18
|
+
if (specifier.type === AST_NODE_TYPES.ImportSpecifier && (specifier.imported.type === AST_NODE_TYPES.Identifier && specifier.imported.name === luminaJsxExportName || specifier.imported.type === AST_NODE_TYPES.Literal && specifier.imported.value === luminaJsxExportName)) {
|
|
19
|
+
withProperty.isLuminaJsx = true;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const withProperty = ImportDeclaration;
|
|
25
|
+
return withProperty;
|
|
26
|
+
}
|
|
27
|
+
function hasDecorator(node, decoratorName) {
|
|
28
|
+
return node.decorators.some(
|
|
29
|
+
(decorator) => decorator.expression.type === AST_NODE_TYPES.CallExpression && decorator.expression.callee.type === AST_NODE_TYPES.Identifier && decorator.expression.callee.name === decoratorName
|
|
30
|
+
) ?? false;
|
|
31
|
+
}
|
|
32
|
+
function extractDeclareElementsInterface(node) {
|
|
33
|
+
return node.kind === "global" ? node.body.body.find(
|
|
34
|
+
(node2) => node2.type === AST_NODE_TYPES.TSInterfaceDeclaration && node2.id.name === "DeclareElements"
|
|
35
|
+
) : void 0;
|
|
36
|
+
}
|
|
37
|
+
function isCreateEvent(node) {
|
|
38
|
+
return node.value?.type === AST_NODE_TYPES.CallExpression && node.value.callee.type === AST_NODE_TYPES.Identifier && node.value.callee.name === "createEvent" && !node.static;
|
|
39
|
+
}
|
|
40
|
+
const getProperty = (properties, name) => properties?.find(
|
|
41
|
+
(option) => option.type === AST_NODE_TYPES.Property && option.key.type === AST_NODE_TYPES.Identifier && option.key.name === name
|
|
42
|
+
)?.value;
|
|
43
|
+
function isGetterWithoutSetter(node) {
|
|
44
|
+
const isGetter = node.type === AST_NODE_TYPES.MethodDefinition && node.kind === "get";
|
|
45
|
+
if (!isGetter) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const index = node.parent.body.indexOf(node);
|
|
49
|
+
const previousNode = node.parent.body.at(index - 1);
|
|
50
|
+
const nextNode = node.parent.body.at(index + 1);
|
|
51
|
+
const name = getName(node);
|
|
52
|
+
if (name === void 0) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const previousIsSetter = previousNode?.type === AST_NODE_TYPES.MethodDefinition && previousNode.kind === "set" && getName(previousNode) === name;
|
|
56
|
+
if (previousIsSetter) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const nextIsSetter = nextNode?.type === AST_NODE_TYPES.MethodDefinition && nextNode.kind === "set" && getName(nextNode) === name;
|
|
60
|
+
return !nextIsSetter;
|
|
61
|
+
}
|
|
62
|
+
function getName(node) {
|
|
63
|
+
if (node.key.type === AST_NODE_TYPES.Identifier) {
|
|
64
|
+
return node.key.name;
|
|
65
|
+
} else if (node.key.type === AST_NODE_TYPES.Literal && typeof node.key.value === "string") {
|
|
66
|
+
return node.key.value;
|
|
67
|
+
} else {
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function parsePropertyDecorator(decorator) {
|
|
72
|
+
const isPropertyDecorator = decorator.expression.type === AST_NODE_TYPES.CallExpression && decorator.expression.callee.type === AST_NODE_TYPES.Identifier && decorator.expression.callee.name === "property";
|
|
73
|
+
if (!isPropertyDecorator) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const callExpression = decorator?.expression.type === AST_NODE_TYPES.CallExpression ? decorator.expression : void 0;
|
|
77
|
+
if (callExpression === void 0) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const options = callExpression.arguments[0]?.type === AST_NODE_TYPES.ObjectExpression ? callExpression.arguments[0] : void 0;
|
|
81
|
+
const properties = options?.properties;
|
|
82
|
+
return {
|
|
83
|
+
callExpression,
|
|
84
|
+
options,
|
|
85
|
+
properties
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function isBindThisCallee(callee) {
|
|
89
|
+
return callee.type === AST_NODE_TYPES.MemberExpression && // expression.identifier(this)
|
|
90
|
+
callee.property.type === AST_NODE_TYPES.Identifier && // expression.bind(this)
|
|
91
|
+
callee.property.name === "bind" && // expression.expression.bind(this)
|
|
92
|
+
callee.object.type === AST_NODE_TYPES.MemberExpression && // expression.identifier.bind(this)
|
|
93
|
+
callee.object.property.type === AST_NODE_TYPES.Identifier && // this.identifier.bind(this)
|
|
94
|
+
callee.object.object.type === AST_NODE_TYPES.ThisExpression;
|
|
95
|
+
}
|
|
96
|
+
const importDeclaration = `import { ${luminaJsxExportName} } from "${luminaEntrypointName}";`;
|
|
97
|
+
const description$h = `To use Lumina's JSX, you need to ${importDeclaration}`;
|
|
98
|
+
const addMissingJsxImport = createRule({
|
|
99
|
+
name: "add-missing-jsx-import",
|
|
100
|
+
meta: {
|
|
101
|
+
docs: {
|
|
102
|
+
description: description$h,
|
|
103
|
+
defaultLevel: "error"
|
|
104
|
+
},
|
|
105
|
+
messages: {
|
|
106
|
+
addMissingJsxImport: description$h
|
|
107
|
+
},
|
|
108
|
+
type: "problem",
|
|
109
|
+
schema: [],
|
|
110
|
+
fixable: "code"
|
|
111
|
+
},
|
|
112
|
+
defaultOptions: [],
|
|
113
|
+
create(context) {
|
|
114
|
+
let errorAlreadyReported = false;
|
|
115
|
+
let isUsingLumina = false;
|
|
116
|
+
let lastImportDeclaration;
|
|
117
|
+
let lastLuminaImportClause;
|
|
118
|
+
let hasLuminaJsxImport = false;
|
|
119
|
+
return {
|
|
120
|
+
ImportDeclaration(node) {
|
|
121
|
+
lastImportDeclaration = node;
|
|
122
|
+
if (node.source.value === luminaTestEntrypointName) {
|
|
123
|
+
isUsingLumina = true;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (node.source.value !== luminaEntrypointName) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
isUsingLumina = true;
|
|
130
|
+
const isTypeOnly = node.importKind === "type";
|
|
131
|
+
for (const specifier of node.specifiers) {
|
|
132
|
+
if (!isTypeOnly) {
|
|
133
|
+
lastLuminaImportClause = specifier;
|
|
134
|
+
}
|
|
135
|
+
if (specifier.type === AST_NODE_TYPES.ImportSpecifier && (specifier.imported.type === AST_NODE_TYPES.Identifier && specifier.imported.name === luminaJsxExportName || specifier.imported.type === AST_NODE_TYPES.Literal && specifier.imported.value === luminaJsxExportName)) {
|
|
136
|
+
hasLuminaJsxImport = true;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
JSXIdentifier(node) {
|
|
142
|
+
if (isUsingLumina && !hasLuminaJsxImport && !errorAlreadyReported) {
|
|
143
|
+
errorAlreadyReported = true;
|
|
144
|
+
context.report({
|
|
145
|
+
messageId: "addMissingJsxImport",
|
|
146
|
+
node,
|
|
147
|
+
fix(fixer) {
|
|
148
|
+
if (lastLuminaImportClause !== void 0) {
|
|
149
|
+
return fixer.insertTextAfter(lastLuminaImportClause, `, ${luminaJsxExportName}`);
|
|
150
|
+
}
|
|
151
|
+
if (lastImportDeclaration !== void 0) {
|
|
152
|
+
return fixer.insertTextAfter(lastImportDeclaration, importDeclaration);
|
|
153
|
+
} else {
|
|
154
|
+
return fixer.insertTextBefore(context.sourceCode.ast, importDeclaration);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
const description$g = "Auto add { type: Boolean } or { type: Number } where necessary";
|
|
164
|
+
const autoAddType = createRule({
|
|
165
|
+
name: "auto-add-type",
|
|
166
|
+
meta: {
|
|
167
|
+
docs: {
|
|
168
|
+
description: description$g,
|
|
169
|
+
defaultLevel: "warn"
|
|
170
|
+
},
|
|
171
|
+
messages: {
|
|
172
|
+
addType: `This property is of {{ type }} type, yet the type is not trivially inferrable from the AST without type-checking. Such properties require a { type: {{ type }} } annotation.
|
|
173
|
+
|
|
174
|
+
More information: https://devtopia.esri.com/WebGIS/arcgis-web-components/issues/1991`,
|
|
175
|
+
typeAnnotationMismatchesActualType: "The @property({type: {{type}} }) doesn't match the actual property type ({{actualType}}).",
|
|
176
|
+
noEmptyPropertyObject: "Replace @property({}) with @property()",
|
|
177
|
+
noUnnecessaryType: "Property type is trivially inferrable without type-checking - remove needless { type: {{ type }} } annotation.",
|
|
178
|
+
unhandledType: `The default Lit attribute<-->property converter does not define any behavior for @property({ type: {{type}} }). Consider removing this type annotation, or adding a custom converter @property({ converter: ... }) to handle this type`
|
|
179
|
+
},
|
|
180
|
+
type: "problem",
|
|
181
|
+
schema: [],
|
|
182
|
+
fixable: "code"
|
|
183
|
+
},
|
|
184
|
+
defaultOptions: [],
|
|
185
|
+
create(context) {
|
|
186
|
+
const services = ESLintUtils.getParserServices(context);
|
|
187
|
+
return {
|
|
188
|
+
Decorator(decorator) {
|
|
189
|
+
const part = parsePropertyDecorator(decorator);
|
|
190
|
+
if (part === void 0) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const { options, properties, callExpression } = part;
|
|
194
|
+
const property = decorator.parent;
|
|
195
|
+
if (property?.type !== AST_NODE_TYPES.MethodDefinition && property?.type !== AST_NODE_TYPES.PropertyDefinition) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const trivialType = inferTrivialType(property);
|
|
199
|
+
const typeProperty = getProperty(properties, "type");
|
|
200
|
+
const converterProperty = getProperty(properties, "converter");
|
|
201
|
+
const isTrivialType = trivialType === "Number" || trivialType === "Boolean";
|
|
202
|
+
if (isTrivialType && typeProperty !== void 0 && typeProperty.type === AST_NODE_TYPES.Identifier && (typeProperty.name === "Number" || typeProperty.name === "Boolean")) {
|
|
203
|
+
context.report({
|
|
204
|
+
node: typeProperty,
|
|
205
|
+
messageId: "noUnnecessaryType",
|
|
206
|
+
data: {
|
|
207
|
+
type: typeProperty.name
|
|
208
|
+
},
|
|
209
|
+
fix(fixer) {
|
|
210
|
+
const nextToken = context.sourceCode.getTokenAfter(typeProperty.parent);
|
|
211
|
+
const isTrailingComma = nextToken && nextToken.value === ",";
|
|
212
|
+
return isTrailingComma ? fixer.removeRange([typeProperty.parent.range[0], nextToken.range[1]]) : fixer.remove(typeProperty.parent);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (
|
|
218
|
+
// Do not emit any warnings if has "converter" as we don't know what the converter does
|
|
219
|
+
converterProperty === void 0 && typeProperty?.type === AST_NODE_TYPES.Identifier && !builtInConverterTypes.has(typeProperty.name)
|
|
220
|
+
) {
|
|
221
|
+
context.report({
|
|
222
|
+
node: typeProperty,
|
|
223
|
+
messageId: "unhandledType",
|
|
224
|
+
data: {
|
|
225
|
+
type: typeProperty.name
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (isTrivialType) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (options !== void 0 && properties?.length === 0) {
|
|
234
|
+
context.report({
|
|
235
|
+
node: options,
|
|
236
|
+
messageId: "noEmptyPropertyObject",
|
|
237
|
+
fix(fixer) {
|
|
238
|
+
return fixer.replaceText(
|
|
239
|
+
options,
|
|
240
|
+
context.sourceCode.getCommentsInside(options).map((comment) => context.sourceCode.getText(comment)).join("\n")
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const attributeOption = getProperty(properties, "attribute");
|
|
247
|
+
const isAttributeFalse = attributeOption?.type === AST_NODE_TYPES.Literal && attributeOption.value === false;
|
|
248
|
+
if (isAttributeFalse) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const type = services.getTypeAtLocation(property);
|
|
252
|
+
const typeFlags = (type.flags & ts.TypeFlags.Union ? type.types : [type]).reduce(
|
|
253
|
+
(flags, type2) => flags | type2.flags,
|
|
254
|
+
0
|
|
255
|
+
);
|
|
256
|
+
const isTypeCastable = (typeFlags & (ts.TypeFlags.String | ts.TypeFlags.Any | ts.TypeFlags.Unknown)) === 0;
|
|
257
|
+
const isNumberType = isTypeCastable && typeFlags & ts.TypeFlags.NumberLike;
|
|
258
|
+
const isBooleanType = isTypeCastable && typeFlags & ts.TypeFlags.BooleanLike;
|
|
259
|
+
if (isNumberType && isBooleanType) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (!isNumberType && !isBooleanType) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (typeProperty !== void 0) {
|
|
266
|
+
if (
|
|
267
|
+
// Do not emit any warnings if has "converter" as we don't know what the converter does
|
|
268
|
+
converterProperty === void 0 && typeProperty.type === AST_NODE_TYPES.Identifier && (typeProperty.name === "Number" && isBooleanType || typeProperty.name === "Boolean" && isNumberType)
|
|
269
|
+
) {
|
|
270
|
+
context.report({
|
|
271
|
+
node: typeProperty,
|
|
272
|
+
messageId: "typeAnnotationMismatchesActualType",
|
|
273
|
+
data: {
|
|
274
|
+
type: typeProperty.name,
|
|
275
|
+
actualType: isNumberType ? "Number" : "Boolean"
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const comments = context.sourceCode.getCommentsBefore(property);
|
|
282
|
+
const isDocsOnlyReadOnly = comments.some((comment) => comment.value.includes("@readonly"));
|
|
283
|
+
const readOnlyProperty = getProperty(properties, "readOnly");
|
|
284
|
+
const hasReadOnlyFlag = readOnlyProperty?.type === AST_NODE_TYPES.Literal && readOnlyProperty.value === true;
|
|
285
|
+
const isReadOnly = isDocsOnlyReadOnly || hasReadOnlyFlag || isGetterWithoutSetter(property);
|
|
286
|
+
const reflectsProperty = getProperty(properties, "reflects");
|
|
287
|
+
if (isReadOnly && (!isBooleanType || reflectsProperty?.type !== AST_NODE_TYPES.Literal || reflectsProperty.value !== true)) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
context.report({
|
|
291
|
+
messageId: "addType",
|
|
292
|
+
data: {
|
|
293
|
+
type: isNumberType ? "Number" : "Boolean"
|
|
294
|
+
},
|
|
295
|
+
node: property,
|
|
296
|
+
fix(fixer) {
|
|
297
|
+
const sourceCode = context.sourceCode;
|
|
298
|
+
if (options === void 0) {
|
|
299
|
+
const token = sourceCode.getTokenAfter(callExpression.callee, {
|
|
300
|
+
filter: (token2) => token2.value === "("
|
|
301
|
+
});
|
|
302
|
+
return fixer.insertTextAfterRange(token.range, `{ type: ${isNumberType ? "Number" : "Boolean"} }`);
|
|
303
|
+
} else {
|
|
304
|
+
return fixer.insertTextBefore(options.properties[0], `type: ${isNumberType ? "Number" : "Boolean"}, `);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
function inferTrivialType(member) {
|
|
313
|
+
const type = member.type === AST_NODE_TYPES.PropertyDefinition ? member.typeAnnotation?.typeAnnotation : member.type === AST_NODE_TYPES.MethodDefinition && member.kind === "set" ? member.value.params[0]?.typeAnnotation?.typeAnnotation : member.type === AST_NODE_TYPES.MethodDefinition && member.kind === "get" ? member.value.returnType?.typeAnnotation : void 0;
|
|
314
|
+
if (type === void 0) {
|
|
315
|
+
if (member.value?.type === AST_NODE_TYPES.Literal && typeof member.value.value === "number" || member.value?.type === AST_NODE_TYPES.UnaryExpression && member.value.argument.type === AST_NODE_TYPES.Literal && typeof member.value.argument.value === "number" && member.value.operator === "-") {
|
|
316
|
+
return "Number";
|
|
317
|
+
}
|
|
318
|
+
if (member.value?.type === AST_NODE_TYPES.Literal && typeof member.value.value === "boolean") {
|
|
319
|
+
return "Boolean";
|
|
320
|
+
}
|
|
321
|
+
} else if (type.type === AST_NODE_TYPES.TSNumberKeyword) {
|
|
322
|
+
return "Number";
|
|
323
|
+
} else if (type.type === AST_NODE_TYPES.TSBooleanKeyword) {
|
|
324
|
+
return "Boolean";
|
|
325
|
+
}
|
|
326
|
+
return "Other";
|
|
327
|
+
}
|
|
328
|
+
const builtInConverterTypes = /* @__PURE__ */ new Set(["Number", "Boolean", "Array", "Object"]);
|
|
329
|
+
const description$f = `Lumina component must be declared in a TSX file with a matching folder name located inside of src/components folder.`;
|
|
330
|
+
const componentPlacementRules = createRule({
|
|
331
|
+
name: "component-placement-rules",
|
|
332
|
+
meta: {
|
|
333
|
+
docs: {
|
|
334
|
+
description: description$f,
|
|
335
|
+
defaultLevel: "error"
|
|
336
|
+
},
|
|
337
|
+
messages: {
|
|
338
|
+
fileFolderNameMismatch: "Lumina component must be declared in a file whose name (without extension) matches the parent folder name.\n\nCreating components in nested folders is supported as long as the file name matches the immediate parent folder name.",
|
|
339
|
+
extensionNotTsx: "Lumina component must be declared in a .tsx file.",
|
|
340
|
+
noComponentOutsideSrcComponents: "All lumina components must be declared within the src/components folder. Inside that folder, you can create sub-folders for structuring component files."
|
|
341
|
+
},
|
|
342
|
+
type: "problem",
|
|
343
|
+
schema: [],
|
|
344
|
+
fixable: "code"
|
|
345
|
+
},
|
|
346
|
+
defaultOptions: [],
|
|
347
|
+
create(context) {
|
|
348
|
+
return {
|
|
349
|
+
TSModuleDeclaration(node) {
|
|
350
|
+
const luminaDeclarationInterface = extractDeclareElementsInterface(node);
|
|
351
|
+
if (luminaDeclarationInterface === void 0) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const filePath = context.filename.replaceAll("\\", "/");
|
|
355
|
+
const containsSrcComponents = filePath.includes("/src/components/");
|
|
356
|
+
if (!containsSrcComponents) {
|
|
357
|
+
context.report({ messageId: "noComponentOutsideSrcComponents", node: luminaDeclarationInterface });
|
|
358
|
+
}
|
|
359
|
+
const isFileExtensionTsx = filePath.endsWith(".tsx");
|
|
360
|
+
if (!isFileExtensionTsx) {
|
|
361
|
+
context.report({ messageId: "extensionNotTsx", node: luminaDeclarationInterface });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const [fileName, folderName] = filePath.split("/").reverse();
|
|
365
|
+
const extensionlessFileName = fileName.slice(0, -".tsx".length);
|
|
366
|
+
if (folderName !== extensionlessFileName) {
|
|
367
|
+
context.report({ messageId: "fileFolderNameMismatch", node: luminaDeclarationInterface });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
const description$e = `Enforce consistent event naming.`;
|
|
374
|
+
const defaultOptions$1 = [
|
|
375
|
+
{
|
|
376
|
+
eventNamespaces: ["arcgis"],
|
|
377
|
+
includeComponentNameInEventName: false
|
|
378
|
+
}
|
|
379
|
+
];
|
|
380
|
+
const consistentEventNaming = createRule({
|
|
381
|
+
name: "consistent-event-naming",
|
|
382
|
+
meta: {
|
|
383
|
+
docs: {
|
|
384
|
+
description: description$e,
|
|
385
|
+
defaultLevel: "warn"
|
|
386
|
+
},
|
|
387
|
+
messages: {
|
|
388
|
+
eventNamespaceError: `Custom event name must start with one of the following prefixes: {{ prefixes }}.
|
|
389
|
+
|
|
390
|
+
Details: https://qawebgis.esri.com/components/lumina/events#best-practices-around-emitting-events`,
|
|
391
|
+
componentNameInEventError: `For consistency, event name should not start with component name.
|
|
392
|
+
|
|
393
|
+
Discussion: https://devtopia.esri.com/WebGIS/arcgis-web-components/discussions/307`,
|
|
394
|
+
noComponentNameInEventError: `For consistency, event name should include component name.`,
|
|
395
|
+
missingPrivateJsDocTag: `Internal and private events must be marked with @internal or @private JSDoc tag.`
|
|
396
|
+
},
|
|
397
|
+
type: "problem",
|
|
398
|
+
schema: [
|
|
399
|
+
{
|
|
400
|
+
type: "object",
|
|
401
|
+
properties: {
|
|
402
|
+
eventNamespaces: {
|
|
403
|
+
type: "array",
|
|
404
|
+
items: {
|
|
405
|
+
type: "string"
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
includeComponentNameInEventName: {
|
|
409
|
+
type: "boolean"
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
additionalProperties: false
|
|
413
|
+
}
|
|
414
|
+
]
|
|
415
|
+
},
|
|
416
|
+
defaultOptions: defaultOptions$1,
|
|
417
|
+
create(context, options) {
|
|
418
|
+
const eventNamespaces = options[0].eventNamespaces;
|
|
419
|
+
const includeComponentNameInEventName = options[0].includeComponentNameInEventName;
|
|
420
|
+
return {
|
|
421
|
+
PropertyDefinition(node) {
|
|
422
|
+
const isLuminaEvent = isCreateEvent(node);
|
|
423
|
+
if (!isLuminaEvent) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const comments = context.sourceCode.getCommentsBefore(node);
|
|
427
|
+
const isDeprecated = comments.some((comment) => comment.value.includes("@deprecated"));
|
|
428
|
+
if (isDeprecated) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const componentName = context.sourceCode.getAncestors(node).find((ancestor) => ancestor.type === AST_NODE_TYPES.ClassDeclaration)?.id?.name;
|
|
432
|
+
if (componentName === void 0) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const propertyName = node.key.type === AST_NODE_TYPES.Identifier ? node.key.name : node.key.type === AST_NODE_TYPES.Literal ? node.key.value : void 0;
|
|
436
|
+
if (typeof propertyName !== "string") {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
let eventName = propertyName;
|
|
440
|
+
if (eventNamespaces.length > 0) {
|
|
441
|
+
const currentNamespace = eventNamespaces.find((namespace) => eventName.startsWith(namespace));
|
|
442
|
+
if (currentNamespace === void 0) {
|
|
443
|
+
context.report({
|
|
444
|
+
messageId: "eventNamespaceError",
|
|
445
|
+
data: {
|
|
446
|
+
prefixes: eventNamespaces.join(", ")
|
|
447
|
+
},
|
|
448
|
+
node: node.key
|
|
449
|
+
});
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
eventName = eventName.slice(currentNamespace.length);
|
|
453
|
+
}
|
|
454
|
+
const startsWithInternal = eventName.toLowerCase().startsWith("internal");
|
|
455
|
+
if (startsWithInternal || eventName.toLowerCase().startsWith("private")) {
|
|
456
|
+
const hasInternalJsDocTag = comments.some((comment) => comment.value.includes("@internal"));
|
|
457
|
+
const hasPrivateJsDocTag = comments.some((comment) => comment.value.includes("@private"));
|
|
458
|
+
if (!hasInternalJsDocTag && !hasPrivateJsDocTag) {
|
|
459
|
+
context.report({
|
|
460
|
+
messageId: "missingPrivateJsDocTag",
|
|
461
|
+
node: node.key
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
eventName = startsWithInternal ? eventName.slice("internal".length) : eventName.slice("private".length);
|
|
465
|
+
}
|
|
466
|
+
const capitalIndex = capitalAfterLower.exec(componentName)?.index;
|
|
467
|
+
const unNamespacedComponentName = capitalIndex === void 0 ? void 0 : componentName.slice(capitalIndex) || void 0;
|
|
468
|
+
const includesComponentName = eventName.startsWith(componentName) || unNamespacedComponentName !== void 0 && eventName.startsWith(unNamespacedComponentName);
|
|
469
|
+
if (includeComponentNameInEventName && !includesComponentName) {
|
|
470
|
+
context.report({
|
|
471
|
+
messageId: "noComponentNameInEventError",
|
|
472
|
+
node: node.key
|
|
473
|
+
});
|
|
474
|
+
} else if (!includeComponentNameInEventName && includesComponentName) {
|
|
475
|
+
context.report({
|
|
476
|
+
messageId: "componentNameInEventError",
|
|
477
|
+
node: node.key
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
const capitalAfterLower = /(?<=[a-z\d])[A-Z]/u;
|
|
485
|
+
const description$d = `Enforce that @property(), @method() and createEvent() members are used in the correct context.`;
|
|
486
|
+
const decoratorsContext = createRule({
|
|
487
|
+
name: "decorators-context",
|
|
488
|
+
meta: {
|
|
489
|
+
docs: {
|
|
490
|
+
description: description$d,
|
|
491
|
+
defaultLevel: "error"
|
|
492
|
+
},
|
|
493
|
+
messages: {
|
|
494
|
+
publicApiMustBePublic: `@property(), @method() and createEvent() members must not have private or protected modifier.
|
|
495
|
+
|
|
496
|
+
If you wish to hide this member from public documentation, use @private or @protected JSDoc tags instead. Documentation: https://qawebgis.esri.com/components/lumina/documenting-components#excluding-api-from-public-documentation`,
|
|
497
|
+
noPropertyDecoratorOnMethods: `Methods must not have @property() nor @state() decorator. Did you mean @property() instead?`,
|
|
498
|
+
noCombinedPropertyEvent: `Property may either be an event (initialized with createEvent()) or a property (has @property() decorator), but not both`,
|
|
499
|
+
noCombinedPropertyState: `Property may either be a state (initialized with @state()) or a property (has @property() decorator), but not both`,
|
|
500
|
+
noComputedName: `Computed property names are not allowed as they are not statically analyzable`
|
|
501
|
+
},
|
|
502
|
+
type: "problem",
|
|
503
|
+
schema: [],
|
|
504
|
+
fixable: "code"
|
|
505
|
+
},
|
|
506
|
+
defaultOptions: [],
|
|
507
|
+
create(context) {
|
|
508
|
+
return {
|
|
509
|
+
PropertyDefinition(node) {
|
|
510
|
+
const hasPropertyDecorator = hasDecorator(node, "property");
|
|
511
|
+
const isPrivateOrProtected = node.accessibility === "private" || node.accessibility === "protected";
|
|
512
|
+
if (hasPropertyDecorator && isPrivateOrProtected) {
|
|
513
|
+
context.report({ messageId: "publicApiMustBePublic", node });
|
|
514
|
+
}
|
|
515
|
+
const isLuminaEvent = isCreateEvent(node);
|
|
516
|
+
if (isLuminaEvent && isPrivateOrProtected) {
|
|
517
|
+
context.report({ messageId: "publicApiMustBePublic", node });
|
|
518
|
+
}
|
|
519
|
+
if (isLuminaEvent && hasPropertyDecorator) {
|
|
520
|
+
context.report({ messageId: "noCombinedPropertyEvent", node });
|
|
521
|
+
}
|
|
522
|
+
const hasStateDecorator = hasDecorator(node, "state");
|
|
523
|
+
if (hasStateDecorator && hasPropertyDecorator) {
|
|
524
|
+
context.report({ messageId: "noCombinedPropertyState", node });
|
|
525
|
+
}
|
|
526
|
+
if (hasPropertyDecorator && node.computed) {
|
|
527
|
+
context.report({ messageId: "noComputedName", node });
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
MethodDefinition(node) {
|
|
531
|
+
const hasMethodDecorator = hasDecorator(node, "method");
|
|
532
|
+
const hasPropertyDecorator = hasDecorator(node, "property");
|
|
533
|
+
const hasStateDecorator = hasDecorator(node, "state");
|
|
534
|
+
const isPrivateOrProtected = node.accessibility === "private" || node.accessibility === "protected";
|
|
535
|
+
if (hasMethodDecorator && isPrivateOrProtected) {
|
|
536
|
+
context.report({ messageId: "publicApiMustBePublic", node });
|
|
537
|
+
}
|
|
538
|
+
if (hasMethodDecorator && (hasPropertyDecorator || hasStateDecorator)) {
|
|
539
|
+
context.report({ messageId: "noPropertyDecoratorOnMethods", node });
|
|
540
|
+
}
|
|
541
|
+
if (hasStateDecorator && hasPropertyDecorator) {
|
|
542
|
+
context.report({ messageId: "noCombinedPropertyState", node });
|
|
543
|
+
}
|
|
544
|
+
const isRealMethod = node.kind === "method";
|
|
545
|
+
if (isRealMethod && (hasPropertyDecorator || hasStateDecorator)) {
|
|
546
|
+
context.report({ messageId: "noPropertyDecoratorOnMethods", node });
|
|
547
|
+
}
|
|
548
|
+
if ((hasPropertyDecorator || hasMethodDecorator) && node.computed) {
|
|
549
|
+
context.report({ messageId: "noComputedName", node });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
const ordering = [
|
|
556
|
+
/*
|
|
557
|
+
* Putting static before instance members is a common OOP convention.
|
|
558
|
+
* Static members are always evaluated before instance members.
|
|
559
|
+
*/
|
|
560
|
+
"Static Members",
|
|
561
|
+
/**
|
|
562
|
+
* Put private properties before public, because private properties are
|
|
563
|
+
* often used as a default value for public properties.
|
|
564
|
+
*/
|
|
565
|
+
"Private Properties",
|
|
566
|
+
/**
|
|
567
|
+
* Put private properties before public, because private properties are often
|
|
568
|
+
* used as a default value for public properties.
|
|
569
|
+
*/
|
|
570
|
+
"State Properties",
|
|
571
|
+
/*
|
|
572
|
+
* Properties are the key component API. Also, since default values of
|
|
573
|
+
* properties evaluate before the constructor, they should logically be before
|
|
574
|
+
* the constructor.
|
|
575
|
+
*/
|
|
576
|
+
"Public Properties",
|
|
577
|
+
/*
|
|
578
|
+
* Methods should be a supplementary component API, not primary, so are listed
|
|
579
|
+
* after properties. At the same time, it is nice to keep public component API
|
|
580
|
+
* all in one place.
|
|
581
|
+
*/
|
|
582
|
+
"Public Methods",
|
|
583
|
+
/*
|
|
584
|
+
* Put events near properties and methods to keep all public component API in
|
|
585
|
+
* one place.
|
|
586
|
+
*/
|
|
587
|
+
"Events",
|
|
588
|
+
/*
|
|
589
|
+
* Lifecycle methods like constructor and connectedCallback are executed
|
|
590
|
+
* after all properties have default values, but before any render functions.
|
|
591
|
+
*/
|
|
592
|
+
"Lifecycle",
|
|
593
|
+
/*
|
|
594
|
+
* Private methods in between lifecycles and rendering as they are often
|
|
595
|
+
* called by either of them.
|
|
596
|
+
*/
|
|
597
|
+
"Private Methods",
|
|
598
|
+
/*
|
|
599
|
+
* Fred:
|
|
600
|
+
* The reason rendering methods are at the bottom of my modules because I can
|
|
601
|
+
* quickly jump to the bottom find the rendering method and work on it. Less
|
|
602
|
+
* scrolling.
|
|
603
|
+
* Once a module has been implemented, most of the bugs/enhancements occur in
|
|
604
|
+
* the rendering methods.
|
|
605
|
+
*/
|
|
606
|
+
"Rendering"
|
|
607
|
+
];
|
|
608
|
+
const getWireframe = () => new Map(ordering.map((region) => [region, []]));
|
|
609
|
+
const entries = Object.entries;
|
|
610
|
+
const regionMatchers = entries({
|
|
611
|
+
"Static Members": (node) => node.type === AST_NODE_TYPES.StaticBlock || "static" in node && node.static,
|
|
612
|
+
"Lifecycle": (_node, name) => lifecyclesSet.has(name),
|
|
613
|
+
"Public Methods": (node) => "decorators" in node && hasDecorator(node, "method"),
|
|
614
|
+
"Public Properties": (node) => "decorators" in node && hasDecorator(node, "property"),
|
|
615
|
+
"State Properties": (node) => "decorators" in node && hasDecorator(node, "state"),
|
|
616
|
+
"Events": isEvent,
|
|
617
|
+
"Rendering": (_node, name) => name.startsWith("render") || name.startsWith("_render"),
|
|
618
|
+
"Private Properties": (node) => node.type === AST_NODE_TYPES.PropertyDefinition || // We don't yet support public accessor properties
|
|
619
|
+
node.type === AST_NODE_TYPES.AccessorProperty || node.type === AST_NODE_TYPES.MethodDefinition && (node.kind === "get" || node.kind === "set"),
|
|
620
|
+
"Private Methods": () => true
|
|
621
|
+
});
|
|
622
|
+
const definitelyComponentRegions = /* @__PURE__ */ new Set([
|
|
623
|
+
"Rendering",
|
|
624
|
+
"State Properties",
|
|
625
|
+
"Public Methods",
|
|
626
|
+
"Events"
|
|
627
|
+
]);
|
|
628
|
+
const regionsArray = regionMatchers.map(([key]) => key);
|
|
629
|
+
function isEvent(node, name) {
|
|
630
|
+
if (node.type !== AST_NODE_TYPES.PropertyDefinition) {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
if (name === "arcgisPropertyChange") {
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
const initializer = node.value;
|
|
637
|
+
if (initializer?.type !== AST_NODE_TYPES.CallExpression) {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
const callExpression = initializer.callee;
|
|
641
|
+
const functionName = callExpression.type === AST_NODE_TYPES.Identifier ? callExpression.name : callExpression.type === AST_NODE_TYPES.CallExpression && callExpression.callee.type === AST_NODE_TYPES.Identifier ? callExpression.callee.name : void 0;
|
|
642
|
+
return functionName === "createEvent" || functionName === "reEmitEvent" || functionName === "usePropertyChange";
|
|
643
|
+
}
|
|
644
|
+
const lifecycles = [
|
|
645
|
+
"constructor",
|
|
646
|
+
"connectedCallback",
|
|
647
|
+
"load",
|
|
648
|
+
"shouldUpdate",
|
|
649
|
+
// Not an actual lifecycle method, but can be temporary inserted by the codemod
|
|
650
|
+
"_shouldUpdate",
|
|
651
|
+
"willUpdate",
|
|
652
|
+
"update",
|
|
653
|
+
"firstUpdated",
|
|
654
|
+
"updated",
|
|
655
|
+
"loaded",
|
|
656
|
+
"disconnectedCallback"
|
|
657
|
+
];
|
|
658
|
+
const lifecyclesSet = new Set(lifecycles);
|
|
659
|
+
const supportedNodeTypes = {
|
|
660
|
+
MethodDefinition: true,
|
|
661
|
+
PropertyDefinition: true,
|
|
662
|
+
AccessorProperty: true,
|
|
663
|
+
StaticBlock: true,
|
|
664
|
+
// These should not be present in non-abstract Lumina component classes
|
|
665
|
+
TSAbstractAccessorProperty: false,
|
|
666
|
+
TSAbstractMethodDefinition: false,
|
|
667
|
+
TSAbstractPropertyDefinition: false,
|
|
668
|
+
TSIndexSignature: false
|
|
669
|
+
};
|
|
670
|
+
function categorizeComments(membersData, sourceCode) {
|
|
671
|
+
const categorized = {
|
|
672
|
+
header: [],
|
|
673
|
+
regionFooter: /* @__PURE__ */ new Map(),
|
|
674
|
+
memberLineEnd: /* @__PURE__ */ new Map(),
|
|
675
|
+
footer: [],
|
|
676
|
+
membersByName: /* @__PURE__ */ new Map()
|
|
677
|
+
};
|
|
678
|
+
let regionName;
|
|
679
|
+
membersData.forEach((memberData, index) => {
|
|
680
|
+
const nameNodes = categorized.membersByName.get(memberData.name);
|
|
681
|
+
if (nameNodes !== void 0) {
|
|
682
|
+
nameNodes.push(memberData);
|
|
683
|
+
} else {
|
|
684
|
+
categorized.membersByName.set(memberData.name, [memberData]);
|
|
685
|
+
}
|
|
686
|
+
memberData.comments = handleLineEndComment(memberData.comments, membersData.at(index - 1), categorized);
|
|
687
|
+
let newRegionName;
|
|
688
|
+
const lastRegionStart = memberData.comments.findLastIndex((comment) => {
|
|
689
|
+
const match = parseRegionComment(comment);
|
|
690
|
+
const isRegionStart = match !== void 0 && match.end === void 0;
|
|
691
|
+
newRegionName = match?.name;
|
|
692
|
+
return isRegionStart;
|
|
693
|
+
});
|
|
694
|
+
if (lastRegionStart !== -1) {
|
|
695
|
+
const commentsBeforeNewRegionStart = memberData.comments.slice(0, lastRegionStart);
|
|
696
|
+
const previousMember = membersData.at(index - 1);
|
|
697
|
+
handleCommentsAroundEndRegion(
|
|
698
|
+
commentsBeforeNewRegionStart,
|
|
699
|
+
false,
|
|
700
|
+
previousMember ?? memberData,
|
|
701
|
+
categorized,
|
|
702
|
+
regionName
|
|
703
|
+
);
|
|
704
|
+
memberData.comments = memberData.comments.slice(lastRegionStart + 1);
|
|
705
|
+
regionName = newRegionName;
|
|
706
|
+
}
|
|
707
|
+
const isLastNode = index === membersData.length - 1;
|
|
708
|
+
if (isLastNode) {
|
|
709
|
+
const trailingComments = sourceCode.getCommentsAfter(memberData.member);
|
|
710
|
+
const remainingTrailingComments = handleLineEndComment(trailingComments, memberData, categorized);
|
|
711
|
+
const previousMember = membersData.at(index - 1);
|
|
712
|
+
handleCommentsAroundEndRegion(
|
|
713
|
+
remainingTrailingComments,
|
|
714
|
+
true,
|
|
715
|
+
previousMember ?? memberData,
|
|
716
|
+
categorized,
|
|
717
|
+
regionName
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
return categorized;
|
|
722
|
+
}
|
|
723
|
+
function handleLineEndComment(comments, previousNode, categorized) {
|
|
724
|
+
if (comments.at(0)?.loc.start.line === previousNode?.member.loc.end.line) {
|
|
725
|
+
categorized.memberLineEnd.set(previousNode, comments.slice(0, 1));
|
|
726
|
+
return comments.slice(1);
|
|
727
|
+
}
|
|
728
|
+
return comments;
|
|
729
|
+
}
|
|
730
|
+
function handleCommentsAroundEndRegion(commentsBeforeNewRegionStart, isFooter, lastMember, categorized, regionName) {
|
|
731
|
+
if (regionName === void 0) {
|
|
732
|
+
const destination = isFooter ? categorized.footer : categorized.header;
|
|
733
|
+
destination.push(...commentsBeforeNewRegionStart);
|
|
734
|
+
} else {
|
|
735
|
+
const firstEndRegion = commentsBeforeNewRegionStart.findIndex(
|
|
736
|
+
(comment) => parseRegionComment(comment)?.end !== void 0
|
|
737
|
+
);
|
|
738
|
+
let previousRegionComments = commentsBeforeNewRegionStart;
|
|
739
|
+
if (firstEndRegion !== -1) {
|
|
740
|
+
categorized.footer.push(...commentsBeforeNewRegionStart.slice(firstEndRegion + 1));
|
|
741
|
+
previousRegionComments = commentsBeforeNewRegionStart.slice(0, firstEndRegion);
|
|
742
|
+
}
|
|
743
|
+
const existingFooter = categorized.regionFooter.get(regionName);
|
|
744
|
+
if (existingFooter === void 0) {
|
|
745
|
+
categorized.regionFooter.set(regionName, { comments: Array.from(previousRegionComments), lastMember });
|
|
746
|
+
} else {
|
|
747
|
+
existingFooter.comments.push(...previousRegionComments);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const normalizeComments = (comments = [], sourceCode, indent = " ") => comments.filter((comment) => parseRegionComment(comment) === void 0).map((comment) => `${indent}${sourceCode.getText(comment)}`).join("\n").replaceAll(reJsApiSectionComment, "");
|
|
752
|
+
const join = (left, right, joiner = "\n\n") => left === "" || right === "" ? right || left : `${left}${joiner}${right}`;
|
|
753
|
+
const reRegionComment = /^ ?#(?<end>end)?region ?(?<name>[^\n]*)$/u;
|
|
754
|
+
const parseRegionComment = (comment) => comment.type === AST_TOKEN_TYPES.Line ? reRegionComment.exec(comment.value)?.groups : void 0;
|
|
755
|
+
const reJsApiSectionComment = /^ \/\/ ?-{4,}\n(?: \/\/\s*\n)? \/\/\s+[^\s][^\n]+\n(?: \/\/\s*\n)? \/\/ ?-{4,}/gmu;
|
|
756
|
+
function getNormalizedRegions(membersByName) {
|
|
757
|
+
const regions = getWireframe();
|
|
758
|
+
membersByName.values().forEach((members) => {
|
|
759
|
+
const lowestRegionIndex = members.reduce(
|
|
760
|
+
(min, { region }) => Math.min(min, regionsArray.indexOf(region)),
|
|
761
|
+
Number.POSITIVE_INFINITY
|
|
762
|
+
);
|
|
763
|
+
if (members.length === 2 && members[0].member.type === AST_NODE_TYPES.MethodDefinition && members[0].member.kind === "set") {
|
|
764
|
+
members.reverse();
|
|
765
|
+
}
|
|
766
|
+
const regionName = regionsArray[lowestRegionIndex];
|
|
767
|
+
regions.get(regionName).push(...members);
|
|
768
|
+
});
|
|
769
|
+
const normalizedRegions = Array.from(regions).filter(([_region, members]) => members.length > 0).map(
|
|
770
|
+
([region, members]) => [region, sortableRegions.has(region) ? members.sort(sortFunction.bind(void 0, region)) : members]
|
|
771
|
+
);
|
|
772
|
+
return normalizedRegions;
|
|
773
|
+
}
|
|
774
|
+
const sortableRegions = /* @__PURE__ */ new Set(["Public Methods", "Events", "Lifecycle"]);
|
|
775
|
+
function sortFunction(region, leftMember, rightMember) {
|
|
776
|
+
let leftName = leftMember.name;
|
|
777
|
+
let rightName = rightMember.name;
|
|
778
|
+
if (region === "Lifecycle") {
|
|
779
|
+
const compare = lifecycles.indexOf(leftName) - lifecycles.indexOf(rightName);
|
|
780
|
+
return compare < 0 ? -1 : 1;
|
|
781
|
+
}
|
|
782
|
+
if (leftName.startsWith("_")) {
|
|
783
|
+
leftName = leftName.slice(1);
|
|
784
|
+
}
|
|
785
|
+
if (rightName.startsWith("_")) {
|
|
786
|
+
rightName = rightName.slice(1);
|
|
787
|
+
}
|
|
788
|
+
if (leftName < rightName) {
|
|
789
|
+
return -1;
|
|
790
|
+
} else if (leftName > rightName) {
|
|
791
|
+
return 1;
|
|
792
|
+
} else {
|
|
793
|
+
return 0;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function reParentOrphanFooterComments(normalizedRegions, regionFooter) {
|
|
797
|
+
const finalRegions = new Set(normalizedRegions.map(([region]) => region));
|
|
798
|
+
regionFooter.forEach(({ comments, lastMember }, originalRegionName) => {
|
|
799
|
+
const regionStillExists = finalRegions.has(originalRegionName);
|
|
800
|
+
if (regionStillExists) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const newFooterCommentsParent = lastMember.region;
|
|
804
|
+
const existingParentFooter = regionFooter.get(newFooterCommentsParent);
|
|
805
|
+
if (existingParentFooter === void 0) {
|
|
806
|
+
regionFooter.set(newFooterCommentsParent, { comments, lastMember });
|
|
807
|
+
} else {
|
|
808
|
+
existingParentFooter.comments.push(...comments);
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
function buildNewBody(normalizedRegions, strayComments, sourceCode) {
|
|
813
|
+
const newRegions = normalizedRegions.map(([region, members]) => {
|
|
814
|
+
const memberLines = members.map((memberData, index) => {
|
|
815
|
+
const { member, comments, name } = memberData;
|
|
816
|
+
const commentsString = normalizeComments(comments, sourceCode);
|
|
817
|
+
const code = sourceCode.getText(member);
|
|
818
|
+
const lineEndComment = normalizeComments(strayComments.memberLineEnd.get(memberData), sourceCode, " ");
|
|
819
|
+
const memberString = ` ${code}${lineEndComment}`;
|
|
820
|
+
const fullMemberString = join(commentsString, memberString, "\n");
|
|
821
|
+
const nextMember = members.at(index + 1);
|
|
822
|
+
const isLast = nextMember === void 0;
|
|
823
|
+
const isNextSameName = name.length > 0 && name === nextMember?.name;
|
|
824
|
+
const separator = isLast ? "" : isNextSameName ? "\n" : "\n\n";
|
|
825
|
+
return `${fullMemberString}${separator}`;
|
|
826
|
+
});
|
|
827
|
+
const footerComments = normalizeComments(strayComments.regionFooter.get(region)?.comments, sourceCode);
|
|
828
|
+
const footer2 = join(footerComments, " //#endregion", "\n\n");
|
|
829
|
+
return [` //#region ${region}`, memberLines.join(""), footer2].join("\n\n");
|
|
830
|
+
});
|
|
831
|
+
const header = normalizeComments(strayComments.header, sourceCode);
|
|
832
|
+
const footer = normalizeComments(strayComments.footer, sourceCode);
|
|
833
|
+
const newBodyContent = join(join(header, newRegions.join("\n\n")), footer);
|
|
834
|
+
const newBody = `{
|
|
835
|
+
${newBodyContent}
|
|
836
|
+
}`;
|
|
837
|
+
return newBody;
|
|
838
|
+
}
|
|
839
|
+
const baseDescription$3 = `Consistently sort component members`;
|
|
840
|
+
const memberOrdering = createRule({
|
|
841
|
+
name: "member-ordering",
|
|
842
|
+
meta: {
|
|
843
|
+
docs: {
|
|
844
|
+
description: baseDescription$3,
|
|
845
|
+
defaultLevel: "warn"
|
|
846
|
+
},
|
|
847
|
+
messages: {
|
|
848
|
+
memberOrdering: "Component member ordering is not consistent. Run ESLint autofix to sort members.",
|
|
849
|
+
unsupportedElementType: "Unsupported class element type: {{ type }}"
|
|
850
|
+
},
|
|
851
|
+
type: "layout",
|
|
852
|
+
schema: [],
|
|
853
|
+
fixable: "whitespace"
|
|
854
|
+
},
|
|
855
|
+
defaultOptions: [],
|
|
856
|
+
create: (context) => ({
|
|
857
|
+
ClassDeclaration(component) {
|
|
858
|
+
const isNamedExport = component.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration;
|
|
859
|
+
if (!isNamedExport || component.abstract) {
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const hasExtends = component.superClass !== null;
|
|
863
|
+
if (!hasExtends) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const sourceCode = context.sourceCode;
|
|
867
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
868
|
+
let currentRegion = "";
|
|
869
|
+
let hasWrongOrdering = false;
|
|
870
|
+
let isDefinitelyComponent = false;
|
|
871
|
+
let hadError = false;
|
|
872
|
+
const membersData = component.body.body.map((member) => {
|
|
873
|
+
const isSupportedType = supportedNodeTypes[member.type];
|
|
874
|
+
if (!isSupportedType) {
|
|
875
|
+
hadError = true;
|
|
876
|
+
context.report({ node: member, messageId: "unsupportedElementType", data: { type: member.type } });
|
|
877
|
+
}
|
|
878
|
+
const name = "key" in member ? getName(member) ?? "" : "";
|
|
879
|
+
const newRegion = regionMatchers.find(([, rules]) => rules(member, name))[0];
|
|
880
|
+
const comments = sourceCode.getCommentsBefore(member);
|
|
881
|
+
if (!hasWrongOrdering) {
|
|
882
|
+
let wrongOrdering = false;
|
|
883
|
+
const previousRegion = currentRegion;
|
|
884
|
+
for (const comment of comments) {
|
|
885
|
+
const region = parseRegionComment(comment);
|
|
886
|
+
if (region === void 0 || region.end !== void 0) {
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
currentRegion = region.name;
|
|
890
|
+
wrongOrdering ||= currentRegion !== newRegion;
|
|
891
|
+
}
|
|
892
|
+
if (
|
|
893
|
+
/*
|
|
894
|
+
* The @property()/@method() decorator might be missing from this
|
|
895
|
+
* node in case of getter/setter or method overload, while still
|
|
896
|
+
* being in correct ordering.
|
|
897
|
+
* The logic in rule body is lightweight compared to more
|
|
898
|
+
* comprehensive in the autofix to keep common case fast, so we
|
|
899
|
+
* ignore possible errors in getter/setters (but those errors would
|
|
900
|
+
* still be fixed as long as there is any other error in the file).
|
|
901
|
+
*/
|
|
902
|
+
!seenNames.has(name) && (member.type !== AST_NODE_TYPES.MethodDefinition || member.kind !== "get" && member.kind !== "set")
|
|
903
|
+
) {
|
|
904
|
+
wrongOrdering ||= currentRegion !== newRegion;
|
|
905
|
+
wrongOrdering ||= previousRegion !== currentRegion && ordering.indexOf(previousRegion) > ordering.indexOf(currentRegion);
|
|
906
|
+
hasWrongOrdering = wrongOrdering;
|
|
907
|
+
}
|
|
908
|
+
seenNames.add(name);
|
|
909
|
+
}
|
|
910
|
+
isDefinitelyComponent ||= definitelyComponentRegions.has(newRegion);
|
|
911
|
+
return { member, name, comments, region: newRegion };
|
|
912
|
+
});
|
|
913
|
+
if (!isDefinitelyComponent) {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (hadError) {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (!hasWrongOrdering) {
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
context.report({
|
|
923
|
+
/**
|
|
924
|
+
* Since this rule is autofixable and stylistic, to be less obtrusive,
|
|
925
|
+
* report the error for a single character only, rather than entire
|
|
926
|
+
* component
|
|
927
|
+
*/
|
|
928
|
+
loc: {
|
|
929
|
+
start: component.loc.end,
|
|
930
|
+
end: component.loc.end
|
|
931
|
+
},
|
|
932
|
+
messageId: "memberOrdering",
|
|
933
|
+
/*
|
|
934
|
+
* We delay as much work as possible till the autofixer is run because
|
|
935
|
+
* most of the time the rule will be run without need for autofixing.
|
|
936
|
+
*/
|
|
937
|
+
fix(fixer) {
|
|
938
|
+
const { membersByName, ...strayComments } = categorizeComments(membersData, sourceCode);
|
|
939
|
+
const normalizedRegions = getNormalizedRegions(membersByName);
|
|
940
|
+
reParentOrphanFooterComments(normalizedRegions, strayComments.regionFooter);
|
|
941
|
+
const newBody = buildNewBody(normalizedRegions, strayComments, sourceCode);
|
|
942
|
+
return fixer.replaceText(component.body, newBody);
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
})
|
|
947
|
+
});
|
|
948
|
+
const description$c = `Use @internal or @private JSDoc tag over @ignore. See https://qawebgis.esri.com/components/lumina/documenting-components#excluding-api-from-public-documentation`;
|
|
949
|
+
const noIgnoreJsDocTag = createRule({
|
|
950
|
+
name: "no-ignore-jsdoc-tag",
|
|
951
|
+
meta: {
|
|
952
|
+
docs: {
|
|
953
|
+
description: description$c,
|
|
954
|
+
defaultLevel: "error"
|
|
955
|
+
},
|
|
956
|
+
messages: {
|
|
957
|
+
noIgnoreJsDocTag: description$c
|
|
958
|
+
},
|
|
959
|
+
type: "problem",
|
|
960
|
+
schema: []
|
|
961
|
+
},
|
|
962
|
+
defaultOptions: [],
|
|
963
|
+
create(context) {
|
|
964
|
+
const luminaJsxCheck = checkForLuminaJsx();
|
|
965
|
+
return {
|
|
966
|
+
"ImportDeclaration": luminaJsxCheck,
|
|
967
|
+
"Program:exit"() {
|
|
968
|
+
if (!luminaJsxCheck.isLuminaJsx) {
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
Array.from(
|
|
972
|
+
context.sourceCode.text.matchAll(reIgnore),
|
|
973
|
+
(match) => context.report({
|
|
974
|
+
messageId: "noIgnoreJsDocTag",
|
|
975
|
+
loc: {
|
|
976
|
+
start: context.sourceCode.getLocFromIndex(match.index + "* ".length),
|
|
977
|
+
end: context.sourceCode.getLocFromIndex(match.index + match[0].length)
|
|
978
|
+
}
|
|
979
|
+
})
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
const reIgnore = /\* @ignore/gu;
|
|
986
|
+
const description$b = `Detect incorrect usage of dynamic JSX tag name`;
|
|
987
|
+
const noIncorrectDynamicTagName = createRule({
|
|
988
|
+
name: "no-incorrect-dynamic-tag-name",
|
|
989
|
+
meta: {
|
|
990
|
+
docs: {
|
|
991
|
+
description: description$b,
|
|
992
|
+
defaultLevel: "error"
|
|
993
|
+
},
|
|
994
|
+
messages: {
|
|
995
|
+
incorrectDynamicTagName: `This is using incorrect dynamic tag name syntax. See documentation on how to use dynamic tag in Lumina's JSX: https://qawebgis.esri.com/components/lumina/jsx#dynamic-tag-name`
|
|
996
|
+
},
|
|
997
|
+
type: "problem",
|
|
998
|
+
schema: []
|
|
999
|
+
},
|
|
1000
|
+
defaultOptions: [],
|
|
1001
|
+
create(context) {
|
|
1002
|
+
const services = ESLintUtils.getParserServices(context);
|
|
1003
|
+
const luminaJsxCheck = checkForLuminaJsx();
|
|
1004
|
+
return {
|
|
1005
|
+
ImportDeclaration: luminaJsxCheck,
|
|
1006
|
+
JSXIdentifier(node) {
|
|
1007
|
+
if (!luminaJsxCheck.isLuminaJsx) {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const isInTagName = node.parent?.type !== AST_NODE_TYPES.JSXAttribute;
|
|
1011
|
+
if (!isInTagName) {
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
const isCapitalized = !node.name.startsWith(node.name.charAt(0).toLowerCase());
|
|
1015
|
+
if (!isCapitalized) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const isCorrectDynamicTagName = node.name === "DynamicHtmlTag" || node.name === "DynamicSvgTag";
|
|
1019
|
+
if (isCorrectDynamicTagName) {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const type = services.getTypeAtLocation(node);
|
|
1023
|
+
const isStringType = type.flags & ts.TypeFlags.StringLike;
|
|
1024
|
+
if (isStringType) {
|
|
1025
|
+
context.report({
|
|
1026
|
+
messageId: "incorrectDynamicTagName",
|
|
1027
|
+
node
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
const baseDescription$2 = `Do not pass an inline arrow function to a ref prop - such syntax creates a new function on each render, which makes Lit call ref callback again on each render.`;
|
|
1035
|
+
const description$a = `${baseDescription$2}
|
|
1036
|
+
|
|
1037
|
+
Alternatives: https://qawebgis.esri.com/components/lumina/jsx#refs`;
|
|
1038
|
+
const noInlineArrowInRef = createRule({
|
|
1039
|
+
name: "no-inline-arrow-in-ref",
|
|
1040
|
+
meta: {
|
|
1041
|
+
docs: {
|
|
1042
|
+
description: baseDescription$2,
|
|
1043
|
+
defaultLevel: "error"
|
|
1044
|
+
},
|
|
1045
|
+
messages: {
|
|
1046
|
+
errorInlineArrow: description$a
|
|
1047
|
+
},
|
|
1048
|
+
type: "problem",
|
|
1049
|
+
schema: []
|
|
1050
|
+
},
|
|
1051
|
+
defaultOptions: [],
|
|
1052
|
+
create(context) {
|
|
1053
|
+
const luminaJsxCheck = checkForLuminaJsx();
|
|
1054
|
+
return {
|
|
1055
|
+
ImportDeclaration: luminaJsxCheck,
|
|
1056
|
+
JSXAttribute(node) {
|
|
1057
|
+
if (!luminaJsxCheck.isLuminaJsx || node.name.name !== "ref" || node.value?.type !== AST_NODE_TYPES.JSXExpressionContainer) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
const initializer = node.value.expression;
|
|
1061
|
+
if (initializer.type !== AST_NODE_TYPES.ArrowFunctionExpression && initializer.type !== AST_NODE_TYPES.FunctionExpression && (initializer.type !== AST_NODE_TYPES.CallExpression || !isBindThisCallee(initializer.callee))) {
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
context.report({
|
|
1065
|
+
messageId: "errorInlineArrow",
|
|
1066
|
+
node: initializer
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
const description$9 = `directives={} prop value must be an array literal. Documentation: https://qawebgis.esri.com/components/lumina/jsx#lit-directives`;
|
|
1073
|
+
const noInvalidDirectivesProp = createRule({
|
|
1074
|
+
name: "no-invalid-directives-prop",
|
|
1075
|
+
meta: {
|
|
1076
|
+
docs: {
|
|
1077
|
+
description: description$9,
|
|
1078
|
+
defaultLevel: "error"
|
|
1079
|
+
},
|
|
1080
|
+
messages: {
|
|
1081
|
+
noInvalidDirectivesProp: description$9
|
|
1082
|
+
},
|
|
1083
|
+
type: "problem",
|
|
1084
|
+
schema: []
|
|
1085
|
+
},
|
|
1086
|
+
defaultOptions: [],
|
|
1087
|
+
create(context) {
|
|
1088
|
+
const luminaJsxCheck = checkForLuminaJsx();
|
|
1089
|
+
return {
|
|
1090
|
+
ImportDeclaration: luminaJsxCheck,
|
|
1091
|
+
JSXAttribute(node) {
|
|
1092
|
+
if (!luminaJsxCheck.isLuminaJsx) {
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (node.name.name !== "directives") {
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const array = node.value?.type === AST_NODE_TYPES.JSXExpressionContainer && node.value.expression.type === AST_NODE_TYPES.ArrayExpression ? node.value.expression : void 0;
|
|
1099
|
+
if (array === void 0 || array.elements.includes(null)) {
|
|
1100
|
+
context.report({
|
|
1101
|
+
messageId: "noInvalidDirectivesProp",
|
|
1102
|
+
node: node.value ?? node
|
|
1103
|
+
});
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
array.elements.forEach((element) => {
|
|
1107
|
+
if (element?.type === AST_NODE_TYPES.SpreadElement) {
|
|
1108
|
+
context.report({
|
|
1109
|
+
messageId: "noInvalidDirectivesProp",
|
|
1110
|
+
node: element
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
const description$8 = `This spread syntax is not supported. Alternatives: https://qawebgis.esri.com/components/lumina/jsx#spread-attributes`;
|
|
1119
|
+
const noJsxSpread = createRule({
|
|
1120
|
+
name: "no-jsx-spread",
|
|
1121
|
+
meta: {
|
|
1122
|
+
docs: {
|
|
1123
|
+
description: description$8,
|
|
1124
|
+
defaultLevel: "error"
|
|
1125
|
+
},
|
|
1126
|
+
messages: {
|
|
1127
|
+
noJsxSpread: description$8
|
|
1128
|
+
},
|
|
1129
|
+
type: "problem",
|
|
1130
|
+
schema: []
|
|
1131
|
+
},
|
|
1132
|
+
defaultOptions: [],
|
|
1133
|
+
create(context) {
|
|
1134
|
+
const luminaJsxCheck = checkForLuminaJsx();
|
|
1135
|
+
return {
|
|
1136
|
+
ImportDeclaration: luminaJsxCheck,
|
|
1137
|
+
JSXSpreadAttribute(node) {
|
|
1138
|
+
if (!luminaJsxCheck.isLuminaJsx) {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
const name = node.parent.name;
|
|
1142
|
+
if (
|
|
1143
|
+
// Spread syntax is allowed in function calls like <this.render {...props} />
|
|
1144
|
+
name.type === AST_NODE_TYPES.JSXIdentifier && // Spread syntax is allowed in functions
|
|
1145
|
+
(name.name.toLowerCase() === name.name || name.name === "DynamicHtmlTag" || name.name === "DynamicSvgTag")
|
|
1146
|
+
) {
|
|
1147
|
+
context.report({
|
|
1148
|
+
messageId: "noJsxSpread",
|
|
1149
|
+
node
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
const baseDescription$1 = `Do not call this.listen()/this.listenOn() in connectedCallback.`;
|
|
1157
|
+
const description$7 = `${baseDescription$1}
|
|
1158
|
+
|
|
1159
|
+
Instead, call this.listen()/this.listenOn() in constructor(), load() or loaded().
|
|
1160
|
+
|
|
1161
|
+
Reason:
|
|
1162
|
+
this.listen() automatically creates and cleanups the listener on connect/disconnect.
|
|
1163
|
+
Since connectedCallback can be called multiple times, duplicate listeners may be created.`;
|
|
1164
|
+
const noListenInConnectedCallback = createRule({
|
|
1165
|
+
name: "no-listen-in-connected-callback",
|
|
1166
|
+
meta: {
|
|
1167
|
+
docs: {
|
|
1168
|
+
description: baseDescription$1,
|
|
1169
|
+
defaultLevel: "error"
|
|
1170
|
+
},
|
|
1171
|
+
messages: {
|
|
1172
|
+
errorListenInConnectedCallback: description$7
|
|
1173
|
+
},
|
|
1174
|
+
type: "problem",
|
|
1175
|
+
schema: []
|
|
1176
|
+
},
|
|
1177
|
+
defaultOptions: [],
|
|
1178
|
+
create(context) {
|
|
1179
|
+
return {
|
|
1180
|
+
CallExpression(node) {
|
|
1181
|
+
const isListenCall = node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.object.type === AST_NODE_TYPES.ThisExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && (node.callee.property.name === "listen" || node.callee.property.name === "listenOn");
|
|
1182
|
+
if (!isListenCall) {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
let hasConnectedCallbackParent = false;
|
|
1186
|
+
let currentParent = node.parent;
|
|
1187
|
+
while (currentParent) {
|
|
1188
|
+
if (currentParent.type === AST_NODE_TYPES.MethodDefinition) {
|
|
1189
|
+
if (currentParent.key.type === AST_NODE_TYPES.Identifier && currentParent.key.name === "connectedCallback") {
|
|
1190
|
+
hasConnectedCallbackParent = true;
|
|
1191
|
+
}
|
|
1192
|
+
break;
|
|
1193
|
+
}
|
|
1194
|
+
currentParent = currentParent.parent;
|
|
1195
|
+
}
|
|
1196
|
+
if (!hasConnectedCallbackParent) {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
context.report({
|
|
1200
|
+
messageId: "errorListenInConnectedCallback",
|
|
1201
|
+
node
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
const description$6 = `To ensure Hot Module Replacement (HMR) works correctly, the file that defines the Lumina component must not export anything other than the component. Exceptions: type-only exports, and the \`exportsForTests\` object for exposing additional things for usages in tests only`;
|
|
1208
|
+
const noNonComponentExports = createRule({
|
|
1209
|
+
name: "no-non-component-exports",
|
|
1210
|
+
meta: {
|
|
1211
|
+
docs: {
|
|
1212
|
+
description: description$6,
|
|
1213
|
+
defaultLevel: "warn"
|
|
1214
|
+
},
|
|
1215
|
+
messages: {
|
|
1216
|
+
noNonComponentExports: description$6,
|
|
1217
|
+
noDefaultExports: `Default exports are not allowed in files that export Lumina component - only named exports are allowed. ${description$6}`,
|
|
1218
|
+
noExportAll: `\`export *\` exports are not allowed in files that export Lumina component - only named exports are allowed. ${description$6}`
|
|
1219
|
+
},
|
|
1220
|
+
type: "problem",
|
|
1221
|
+
schema: []
|
|
1222
|
+
},
|
|
1223
|
+
defaultOptions: [],
|
|
1224
|
+
create(context) {
|
|
1225
|
+
const declaredComponents = /* @__PURE__ */ new Set();
|
|
1226
|
+
const hasLuminaDeclarations = context.sourceCode.text.includes("interface DeclareElements");
|
|
1227
|
+
return {
|
|
1228
|
+
TSModuleDeclaration(node) {
|
|
1229
|
+
const luminaDeclarationInterface = extractDeclareElementsInterface(node);
|
|
1230
|
+
if (luminaDeclarationInterface === void 0) {
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
luminaDeclarationInterface.body.body.forEach((member) => {
|
|
1234
|
+
if (member.type !== AST_NODE_TYPES.TSPropertySignature || member.computed) {
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const type = member.typeAnnotation?.typeAnnotation;
|
|
1238
|
+
if (type?.type !== AST_NODE_TYPES.TSTypeReference || type.typeName.type !== AST_NODE_TYPES.Identifier) {
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
const className = type.typeName.name;
|
|
1242
|
+
declaredComponents.add(className);
|
|
1243
|
+
});
|
|
1244
|
+
},
|
|
1245
|
+
ExportNamedDeclaration(node) {
|
|
1246
|
+
if (!hasLuminaDeclarations && declaredComponents.size === 0) {
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
if (node.exportKind === "type") {
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
if (node.declaration?.type === AST_NODE_TYPES.VariableDeclaration) {
|
|
1253
|
+
const isExportsForTests = node.declaration.declarations.every(
|
|
1254
|
+
(declaration) => declaration.id.type === AST_NODE_TYPES.Identifier && declaration.id.name === "exportsForTests"
|
|
1255
|
+
);
|
|
1256
|
+
if (isExportsForTests) {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
} else if (node.declaration?.type === AST_NODE_TYPES.FunctionDeclaration) {
|
|
1260
|
+
const isExportsForTests = node.declaration.id?.name === "exportsForTests";
|
|
1261
|
+
if (isExportsForTests) {
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
} else if (
|
|
1265
|
+
// Type-only constructs
|
|
1266
|
+
node.declaration?.type === AST_NODE_TYPES.TSDeclareFunction || node.declaration?.type === AST_NODE_TYPES.TSInterfaceDeclaration || node.declaration?.type === AST_NODE_TYPES.TSTypeAliasDeclaration
|
|
1267
|
+
) {
|
|
1268
|
+
return;
|
|
1269
|
+
} else if (node.declaration?.type === AST_NODE_TYPES.ClassDeclaration) {
|
|
1270
|
+
const isComponent = declaredComponents.has(node.declaration.id?.name ?? "");
|
|
1271
|
+
if (isComponent) {
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
context.report({
|
|
1276
|
+
node,
|
|
1277
|
+
messageId: "noNonComponentExports"
|
|
1278
|
+
});
|
|
1279
|
+
},
|
|
1280
|
+
ExportDefaultDeclaration(node) {
|
|
1281
|
+
if (!hasLuminaDeclarations && declaredComponents.size === 0) {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
context.report({
|
|
1285
|
+
node,
|
|
1286
|
+
messageId: "noDefaultExports"
|
|
1287
|
+
});
|
|
1288
|
+
},
|
|
1289
|
+
ExportAllDeclaration(node) {
|
|
1290
|
+
if (!hasLuminaDeclarations && declaredComponents.size === 0) {
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
if (node.exportKind === "type") {
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
context.report({
|
|
1297
|
+
node,
|
|
1298
|
+
messageId: "noExportAll"
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
const description$5 = `Do not start public property names with "on" as that can confuse frameworks into thinking this property is an event`;
|
|
1305
|
+
const noPropertyNameStartWithOn = createRule({
|
|
1306
|
+
name: "no-property-name-start-with-on",
|
|
1307
|
+
meta: {
|
|
1308
|
+
docs: {
|
|
1309
|
+
description: description$5,
|
|
1310
|
+
defaultLevel: "error"
|
|
1311
|
+
},
|
|
1312
|
+
messages: {
|
|
1313
|
+
noPropertyNameStartWithOn: description$5
|
|
1314
|
+
},
|
|
1315
|
+
type: "problem",
|
|
1316
|
+
schema: []
|
|
1317
|
+
},
|
|
1318
|
+
defaultOptions: [],
|
|
1319
|
+
create(context) {
|
|
1320
|
+
return {
|
|
1321
|
+
PropertyDefinition(node) {
|
|
1322
|
+
const isPublicProperty = hasDecorator(node, "property");
|
|
1323
|
+
if (!isPublicProperty) {
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
const propertyName = node.key.type === AST_NODE_TYPES.Identifier ? node.key.name : node.key.type === AST_NODE_TYPES.Literal ? node.key.value : void 0;
|
|
1327
|
+
if (typeof propertyName !== "string") {
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
if (!propertyName.startsWith("on")) {
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
const comments = context.sourceCode.getCommentsBefore(node);
|
|
1334
|
+
const isDeprecated = comments.some((comment) => comment.value.includes("@deprecated"));
|
|
1335
|
+
if (isDeprecated) {
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
context.report({
|
|
1339
|
+
messageId: "noPropertyNameStartWithOn",
|
|
1340
|
+
node: node.key
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
const litTemplateResult = "TemplateResult";
|
|
1347
|
+
const explicitJsxTypeName = "JsxNode";
|
|
1348
|
+
const implicitJsxTypeName = "Element";
|
|
1349
|
+
const implicitJsxParentName = "LuminaJsx";
|
|
1350
|
+
function isLuminaJsxType(type) {
|
|
1351
|
+
for (const declaration of type.aliasSymbol?.declarations ?? []) {
|
|
1352
|
+
if (!ts.isTypeAliasDeclaration(declaration)) {
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
const typeName = declaration.name.escapedText;
|
|
1356
|
+
if (typeName === litTemplateResult || typeName === explicitJsxTypeName) {
|
|
1357
|
+
return true;
|
|
1358
|
+
}
|
|
1359
|
+
if (typeName !== implicitJsxTypeName) {
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
const grandParent = declaration.parent?.parent;
|
|
1363
|
+
if (!ts.isModuleDeclaration(grandParent)) {
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
const symbol = grandParent.symbol;
|
|
1367
|
+
if (symbol?.escapedName === implicitJsxParentName) {
|
|
1368
|
+
return true;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
return false;
|
|
1372
|
+
}
|
|
1373
|
+
const baseDescription = `Avoid accidentally rendering "false" to the screen (Lit stringifies booleans, rather than drop them like React/Stencil).`;
|
|
1374
|
+
const description$4 = `${baseDescription}
|
|
1375
|
+
|
|
1376
|
+
Lumina automatically handles some cases where "false" may get rendered to the screen, but the pattern this code is using is not handled.`;
|
|
1377
|
+
const noRenderFalse = createRule({
|
|
1378
|
+
name: "no-render-false",
|
|
1379
|
+
meta: {
|
|
1380
|
+
docs: {
|
|
1381
|
+
description: baseDescription,
|
|
1382
|
+
defaultLevel: "warn"
|
|
1383
|
+
},
|
|
1384
|
+
messages: {
|
|
1385
|
+
errorFalseRendered: description$4
|
|
1386
|
+
},
|
|
1387
|
+
type: "problem",
|
|
1388
|
+
schema: [],
|
|
1389
|
+
fixable: "code"
|
|
1390
|
+
},
|
|
1391
|
+
defaultOptions: [],
|
|
1392
|
+
create(context) {
|
|
1393
|
+
const services = ESLintUtils.getParserServices(context);
|
|
1394
|
+
return {
|
|
1395
|
+
LogicalExpression(node) {
|
|
1396
|
+
if (node.operator !== "&&") {
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
const type = services.getTypeAtLocation(node.right);
|
|
1400
|
+
if (!isLuminaJsxType(type)) {
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
if (node.parent.type === AST_NODE_TYPES.JSXExpressionContainer) {
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
const right = unwrapExpression(node.right);
|
|
1407
|
+
if (right.type === AST_NODE_TYPES.JSXElement) {
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (node.parent.type === AST_NODE_TYPES.LogicalExpression) {
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
context.report({
|
|
1414
|
+
messageId: "errorFalseRendered",
|
|
1415
|
+
node,
|
|
1416
|
+
fix(fixer) {
|
|
1417
|
+
const leftText = context.sourceCode.getText(node.left);
|
|
1418
|
+
const rightText = context.sourceCode.getText(node.right);
|
|
1419
|
+
const replacement = `${leftText} ? ${rightText} : ""`;
|
|
1420
|
+
return fixer.replaceText(node, replacement);
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
const description$3 = 'There is no need for { attribute: "name" } in @property() when attribute name is trivially inferrable from the property name';
|
|
1428
|
+
const noUnnecessaryAttributeName = createRule({
|
|
1429
|
+
name: "no-unnecessary-attribute-name",
|
|
1430
|
+
meta: {
|
|
1431
|
+
docs: {
|
|
1432
|
+
description: description$3,
|
|
1433
|
+
defaultLevel: "warn"
|
|
1434
|
+
},
|
|
1435
|
+
messages: {
|
|
1436
|
+
noUnnecessaryAttributeName: description$3
|
|
1437
|
+
},
|
|
1438
|
+
type: "suggestion",
|
|
1439
|
+
schema: [],
|
|
1440
|
+
fixable: "code"
|
|
1441
|
+
},
|
|
1442
|
+
defaultOptions: [],
|
|
1443
|
+
create(context) {
|
|
1444
|
+
return {
|
|
1445
|
+
Decorator(decorator) {
|
|
1446
|
+
const part = parsePropertyDecorator(decorator);
|
|
1447
|
+
if (part === void 0) {
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
const { properties } = part;
|
|
1451
|
+
const property = decorator.parent;
|
|
1452
|
+
if (property?.type !== AST_NODE_TYPES.MethodDefinition && property?.type !== AST_NODE_TYPES.PropertyDefinition) {
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
const attributeOption = getProperty(properties, "attribute");
|
|
1456
|
+
const attributeValue = attributeOption?.type === AST_NODE_TYPES.Literal && typeof attributeOption.value === "string" ? attributeOption.value : void 0;
|
|
1457
|
+
if (attributeOption === void 0 || attributeValue === void 0) {
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
const propertyName = getName(property);
|
|
1461
|
+
if (propertyName === void 0) {
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
const inferredName = camelToKebab(propertyName);
|
|
1465
|
+
if (attributeValue === inferredName) {
|
|
1466
|
+
context.report({
|
|
1467
|
+
node: attributeOption,
|
|
1468
|
+
messageId: "noUnnecessaryAttributeName",
|
|
1469
|
+
fix(fixer) {
|
|
1470
|
+
const nextToken = context.sourceCode.getTokenAfter(attributeOption.parent);
|
|
1471
|
+
const isTrailingComma = nextToken && nextToken.value === ",";
|
|
1472
|
+
return isTrailingComma ? fixer.removeRange([attributeOption.parent.range[0], nextToken.range[1]]) : fixer.remove(attributeOption.parent);
|
|
1473
|
+
}
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
const description$2 = `.bind(this) is not necessary in Lit's event listener callbacks and ref callbacks as it is handled automatically.`;
|
|
1481
|
+
const noUnnecessaryBindThis = createRule({
|
|
1482
|
+
name: "no-unnecessary-bind-this",
|
|
1483
|
+
meta: {
|
|
1484
|
+
docs: {
|
|
1485
|
+
description: description$2,
|
|
1486
|
+
defaultLevel: "warn"
|
|
1487
|
+
},
|
|
1488
|
+
messages: {
|
|
1489
|
+
noUnnecessaryBindThis: description$2
|
|
1490
|
+
},
|
|
1491
|
+
type: "problem",
|
|
1492
|
+
schema: [],
|
|
1493
|
+
fixable: "code"
|
|
1494
|
+
},
|
|
1495
|
+
defaultOptions: [],
|
|
1496
|
+
create(context) {
|
|
1497
|
+
const luminaJsxCheck = checkForLuminaJsx();
|
|
1498
|
+
return {
|
|
1499
|
+
ImportDeclaration: luminaJsxCheck,
|
|
1500
|
+
JSXExpressionContainer(node) {
|
|
1501
|
+
if (!luminaJsxCheck.isLuminaJsx || node.parent.type !== AST_NODE_TYPES.JSXAttribute || node.parent.name.type !== AST_NODE_TYPES.JSXIdentifier) {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
const name = node.parent.name.name;
|
|
1505
|
+
const isAutoBindable = name.startsWith("on") || name === "ref";
|
|
1506
|
+
if (!isAutoBindable) {
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
const expression = node.expression.type === AST_NODE_TYPES.ChainExpression ? node.expression.expression : node.expression;
|
|
1510
|
+
const isCallWithThis = (
|
|
1511
|
+
// expression(...)
|
|
1512
|
+
expression.type === AST_NODE_TYPES.CallExpression && // expression(expression)
|
|
1513
|
+
expression.arguments.length === 1 && // expression(this)
|
|
1514
|
+
expression.arguments[0].type === AST_NODE_TYPES.ThisExpression
|
|
1515
|
+
);
|
|
1516
|
+
if (!isCallWithThis) {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
const callee = expression.callee;
|
|
1520
|
+
if (!isBindThisCallee(callee)) {
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
const tagName = context.sourceCode.getAncestors(node.parent).find((ancestor) => ancestor.type === AST_NODE_TYPES.JSXOpeningElement)?.name;
|
|
1524
|
+
const tagNameString = tagName?.type === AST_NODE_TYPES.JSXIdentifier ? tagName.name : void 0;
|
|
1525
|
+
if (tagNameString === void 0) {
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
const isHtmlElement = tagNameString.startsWith(tagNameString.charAt(0).toLowerCase()) || tagNameString === "DynamicHtmlTag" || tagNameString === "DynamicSvgTag";
|
|
1529
|
+
if (!isHtmlElement) {
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
context.report({
|
|
1533
|
+
messageId: "noUnnecessaryBindThis",
|
|
1534
|
+
node: callee.property,
|
|
1535
|
+
fix(fixer) {
|
|
1536
|
+
return fixer.replaceText(expression, context.sourceCode.getText(callee.object));
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
const description$1 = `In most cases, key={index} is not necessary in Lumina in .map(). Details: https://qawebgis.esri.com/components/lumina/jsx#key-prop`;
|
|
1544
|
+
const noUnnecessaryKey = createRule({
|
|
1545
|
+
name: "no-unnecessary-key",
|
|
1546
|
+
meta: {
|
|
1547
|
+
docs: {
|
|
1548
|
+
description: description$1,
|
|
1549
|
+
defaultLevel: "warn"
|
|
1550
|
+
},
|
|
1551
|
+
messages: {
|
|
1552
|
+
noUnnecessaryKey: description$1
|
|
1553
|
+
},
|
|
1554
|
+
type: "suggestion",
|
|
1555
|
+
schema: [],
|
|
1556
|
+
fixable: "code"
|
|
1557
|
+
},
|
|
1558
|
+
defaultOptions: [],
|
|
1559
|
+
create(context) {
|
|
1560
|
+
const luminaJsxCheck = checkForLuminaJsx();
|
|
1561
|
+
return {
|
|
1562
|
+
ImportDeclaration: luminaJsxCheck,
|
|
1563
|
+
JSXExpressionContainer(node) {
|
|
1564
|
+
if (!luminaJsxCheck.isLuminaJsx) {
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const keyValue = node.expression;
|
|
1568
|
+
if (keyValue.type !== AST_NODE_TYPES.Identifier) {
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
const keyAttribute = node.parent;
|
|
1572
|
+
if (keyAttribute?.type !== AST_NODE_TYPES.JSXAttribute || keyAttribute.name.type !== AST_NODE_TYPES.JSXIdentifier || keyAttribute.name.name !== "key") {
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
const mapArrowFunction = context.sourceCode.getAncestors(keyAttribute).find(
|
|
1576
|
+
(ancestor) => ancestor.type === AST_NODE_TYPES.ArrowFunctionExpression && ancestor.parent?.type === AST_NODE_TYPES.CallExpression && ancestor.parent?.callee.type === AST_NODE_TYPES.MemberExpression && ancestor.parent?.callee.property.type === AST_NODE_TYPES.Identifier && ancestor.parent?.callee.property.name === "map"
|
|
1577
|
+
);
|
|
1578
|
+
if (mapArrowFunction === void 0) {
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
const keyParameter = mapArrowFunction.params.at(1);
|
|
1582
|
+
if (keyParameter?.type !== AST_NODE_TYPES.Identifier || keyParameter.name !== keyValue.name) {
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
context.report({
|
|
1586
|
+
messageId: "noUnnecessaryKey",
|
|
1587
|
+
node: keyAttribute,
|
|
1588
|
+
fix(fixer) {
|
|
1589
|
+
const usesIndexOutsideKey = Array.from(
|
|
1590
|
+
context.sourceCode.getText(mapArrowFunction.body).matchAll(new RegExp(`\\b${keyValue.name}\\b`, "ug"))
|
|
1591
|
+
).length > 1;
|
|
1592
|
+
const withoutAttribute = fixer.remove(keyAttribute);
|
|
1593
|
+
if (usesIndexOutsideKey || mapArrowFunction.params.length > 2) {
|
|
1594
|
+
return withoutAttribute;
|
|
1595
|
+
} else {
|
|
1596
|
+
return [withoutAttribute, fixer.remove(keyParameter)];
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
const description = `Validate component tag name`;
|
|
1605
|
+
const defaultOptions = [
|
|
1606
|
+
{
|
|
1607
|
+
namespaces: ["arcgis-"]
|
|
1608
|
+
}
|
|
1609
|
+
];
|
|
1610
|
+
const tagNameRules = createRule({
|
|
1611
|
+
name: "tag-name-rules",
|
|
1612
|
+
meta: {
|
|
1613
|
+
docs: {
|
|
1614
|
+
description,
|
|
1615
|
+
defaultLevel: "error"
|
|
1616
|
+
},
|
|
1617
|
+
messages: {
|
|
1618
|
+
requireNamespace: "Custom element tag names in this project must start with one of the following namespaces: {{namespaces}}",
|
|
1619
|
+
noComputedTagName: "Computed tag names are not allowed",
|
|
1620
|
+
noNonStringTagName: 'Tag names must be strings like "arcgis-click" rather than numbers',
|
|
1621
|
+
noTagNameIdentifier: "Tag name must include a dash",
|
|
1622
|
+
unexpectedDeclareElementsEntry: "Unexpected entry in declare elements interface. Expected a property signature",
|
|
1623
|
+
unexpectedDeclarationType: "Unexpected declaration type. Expected component class identifier",
|
|
1624
|
+
duplicateDeclaration: "The same component may only be assigned to one class name. If you need multiple tag names, consider sub-classing the component",
|
|
1625
|
+
missingClassDeclaration: "Missing class declaration",
|
|
1626
|
+
unexpectedTypeArguments: "Unexpected type arguments in the component tag name declaration. Type arguments should be specified in the component class declaration",
|
|
1627
|
+
reservedTagName: "This is a reserved html element tag name. Declaring components with this name is forbidden by the custom elements specification.",
|
|
1628
|
+
duplicateDeclareGlobal: `There should only be a single "declare global" block in a file. Declaring multiple components in a single file is supported, but they should share the same "declare global {" block. Example:
|
|
1629
|
+
declare global {
|
|
1630
|
+
interface DeclareElements {
|
|
1631
|
+
"arcgis-test1": ArcgisTest1;
|
|
1632
|
+
"arcgis-test2": ArcgisTest2;
|
|
1633
|
+
}
|
|
1634
|
+
}`,
|
|
1635
|
+
mustSubclass: "Lumina component is required to subclass LitElement or a subclass of LitElement",
|
|
1636
|
+
mustNotSubclassHtmlElement: "Lumina component is not allowed to subclass HTMLElement classes directly. Subclass LitElement or a subclass of LitElement instead",
|
|
1637
|
+
tagNameClassNameMismatch: 'The custom element tag name "{{tagName}}" does not seem to match the class name it is assigned to: "{{className}}". Make sure tag name and class name use the same characters in the same order (case insensitive, hyphens removed). {{namespaceNotice}}',
|
|
1638
|
+
missingHyphen: "Custom element tag names need to include a hyphen",
|
|
1639
|
+
lowercaseTagName: "For consistency, custom element tag names should be defined in lowercase",
|
|
1640
|
+
invalidTagName: "Potentially invalid custom element name. Expected tag name to match regex: {{regex}}. This regex is stricter than what the custom elements spec allows in order to catch possible typos. Please contact Lumina maintainers if you need to loosen this check."
|
|
1641
|
+
},
|
|
1642
|
+
type: "problem",
|
|
1643
|
+
schema: [
|
|
1644
|
+
{
|
|
1645
|
+
type: "object",
|
|
1646
|
+
properties: {
|
|
1647
|
+
namespaces: {
|
|
1648
|
+
type: "array",
|
|
1649
|
+
items: {
|
|
1650
|
+
type: "string"
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
},
|
|
1654
|
+
additionalProperties: false
|
|
1655
|
+
}
|
|
1656
|
+
],
|
|
1657
|
+
fixable: "code"
|
|
1658
|
+
},
|
|
1659
|
+
defaultOptions,
|
|
1660
|
+
create(context, options) {
|
|
1661
|
+
const declaredComponents = /* @__PURE__ */ new Map();
|
|
1662
|
+
let seenDeclareGlobal = false;
|
|
1663
|
+
return {
|
|
1664
|
+
"TSModuleDeclaration"(node) {
|
|
1665
|
+
const luminaDeclarationInterface = extractDeclareElementsInterface(node);
|
|
1666
|
+
if (luminaDeclarationInterface === void 0) {
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
if (seenDeclareGlobal) {
|
|
1670
|
+
context.report({
|
|
1671
|
+
messageId: "duplicateDeclareGlobal",
|
|
1672
|
+
node
|
|
1673
|
+
});
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
seenDeclareGlobal = true;
|
|
1677
|
+
luminaDeclarationInterface.body.body.forEach((member) => {
|
|
1678
|
+
if (member.type !== AST_NODE_TYPES.TSPropertySignature) {
|
|
1679
|
+
context.report({
|
|
1680
|
+
messageId: "unexpectedDeclareElementsEntry",
|
|
1681
|
+
node: member
|
|
1682
|
+
});
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
if (member.computed) {
|
|
1686
|
+
context.report({
|
|
1687
|
+
messageId: "noComputedTagName",
|
|
1688
|
+
node: member.key
|
|
1689
|
+
});
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
if (member.key.type === AST_NODE_TYPES.Identifier) {
|
|
1693
|
+
context.report({
|
|
1694
|
+
messageId: "noTagNameIdentifier",
|
|
1695
|
+
node: member.key
|
|
1696
|
+
});
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
if (typeof member.key.value !== "string") {
|
|
1700
|
+
context.report({
|
|
1701
|
+
messageId: "noNonStringTagName",
|
|
1702
|
+
node: member.key
|
|
1703
|
+
});
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
const tagName = member.key.value;
|
|
1707
|
+
let namespaceFreeTagName = tagName;
|
|
1708
|
+
const namespaces = options[0].namespaces;
|
|
1709
|
+
if (namespaces.length > 0) {
|
|
1710
|
+
const namespace = namespaces.find((ns) => tagName.startsWith(ns));
|
|
1711
|
+
if (namespace === void 0) {
|
|
1712
|
+
context.report({
|
|
1713
|
+
messageId: "requireNamespace",
|
|
1714
|
+
node: member.key,
|
|
1715
|
+
data: {
|
|
1716
|
+
namespaces: namespaces.join(", ")
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
return;
|
|
1720
|
+
} else {
|
|
1721
|
+
namespaceFreeTagName = tagName.slice(namespace.length);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
if (!tagName.includes("-")) {
|
|
1725
|
+
context.report({
|
|
1726
|
+
messageId: "missingHyphen",
|
|
1727
|
+
node: member.key
|
|
1728
|
+
});
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
if (tagName.toLowerCase() !== tagName) {
|
|
1732
|
+
context.report({
|
|
1733
|
+
messageId: "lowercaseTagName",
|
|
1734
|
+
node: member.key
|
|
1735
|
+
});
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
if (!reCustomElementName.test(tagName)) {
|
|
1739
|
+
context.report({
|
|
1740
|
+
messageId: "invalidTagName",
|
|
1741
|
+
node: member.key,
|
|
1742
|
+
data: {
|
|
1743
|
+
regex: reCustomElementName.toString()
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
if (blockListedCustomElementNames.has(tagName)) {
|
|
1749
|
+
context.report({
|
|
1750
|
+
messageId: "reservedTagName",
|
|
1751
|
+
node: member.key
|
|
1752
|
+
});
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
const type = member.typeAnnotation?.typeAnnotation;
|
|
1756
|
+
if (type?.type !== AST_NODE_TYPES.TSTypeReference || type.typeName.type !== AST_NODE_TYPES.Identifier) {
|
|
1757
|
+
context.report({
|
|
1758
|
+
messageId: "unexpectedDeclarationType",
|
|
1759
|
+
node: type ?? member
|
|
1760
|
+
});
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
if (type.typeArguments !== void 0) {
|
|
1764
|
+
context.report({
|
|
1765
|
+
messageId: "unexpectedTypeArguments",
|
|
1766
|
+
node: type
|
|
1767
|
+
});
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
const className = type.typeName.name;
|
|
1771
|
+
const classNameFromTagName = tagNameToNormalizedClassName(tagName);
|
|
1772
|
+
const classNameFromShortTagName = tagNameToNormalizedClassName(namespaceFreeTagName);
|
|
1773
|
+
if (classNameFromTagName !== className.toLowerCase() && classNameFromShortTagName !== className.toLowerCase()) {
|
|
1774
|
+
context.report({
|
|
1775
|
+
messageId: "tagNameClassNameMismatch",
|
|
1776
|
+
node: type.typeName,
|
|
1777
|
+
data: {
|
|
1778
|
+
tagName,
|
|
1779
|
+
className,
|
|
1780
|
+
namespaceNotice: namespaces.length > 0 ? `In addition, the following tag name prefixes are expected: ${namespaces.join(", ")}` : ""
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
if (declaredComponents.has(className)) {
|
|
1786
|
+
context.report({
|
|
1787
|
+
messageId: "duplicateDeclaration",
|
|
1788
|
+
node: type.typeName
|
|
1789
|
+
});
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
declaredComponents.set(className, type.typeName);
|
|
1793
|
+
});
|
|
1794
|
+
},
|
|
1795
|
+
"ClassDeclaration"(node) {
|
|
1796
|
+
if (node.id !== null && declaredComponents.has(node.id.name)) {
|
|
1797
|
+
declaredComponents.delete(node.id.name);
|
|
1798
|
+
if (node.superClass === null) {
|
|
1799
|
+
context.report({
|
|
1800
|
+
messageId: "mustSubclass",
|
|
1801
|
+
node
|
|
1802
|
+
});
|
|
1803
|
+
} else if (node.superClass.type === AST_NODE_TYPES.Identifier && node.superClass.name.startsWith("HTML") && node.superClass.name.endsWith("Element")) {
|
|
1804
|
+
context.report({
|
|
1805
|
+
messageId: "mustNotSubclassHtmlElement",
|
|
1806
|
+
node
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
},
|
|
1811
|
+
"Program:exit"() {
|
|
1812
|
+
for (const identifier of declaredComponents.values()) {
|
|
1813
|
+
context.report({
|
|
1814
|
+
messageId: "missingClassDeclaration",
|
|
1815
|
+
node: identifier
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
const blockListedCustomElementNames = /* @__PURE__ */ new Set([
|
|
1823
|
+
"annotation-xml",
|
|
1824
|
+
"color-profile",
|
|
1825
|
+
"font-face",
|
|
1826
|
+
"font-face-src",
|
|
1827
|
+
"font-face-uri",
|
|
1828
|
+
"font-face-format",
|
|
1829
|
+
"font-face-name",
|
|
1830
|
+
"missing-glyph"
|
|
1831
|
+
]);
|
|
1832
|
+
const tagNameToNormalizedClassName = (tagName) => tagName.replaceAll("-", "").toLowerCase();
|
|
1833
|
+
const reCustomElementName = /^[a-z]+(?:-[a-z\d]+)+$/u;
|
|
1834
|
+
const luminaPlugin = makeEslintPlugin("lumina", {
|
|
1835
|
+
"add-missing-jsx-import": addMissingJsxImport,
|
|
1836
|
+
"auto-add-type": autoAddType,
|
|
1837
|
+
"component-placement-rules": componentPlacementRules,
|
|
1838
|
+
"consistent-event-naming": consistentEventNaming,
|
|
1839
|
+
"decorators-context": decoratorsContext,
|
|
1840
|
+
"member-ordering": memberOrdering,
|
|
1841
|
+
"no-ignore-jsdoc-tag": noIgnoreJsDocTag,
|
|
1842
|
+
"no-incorrect-dynamic-tag-name": noIncorrectDynamicTagName,
|
|
1843
|
+
"no-inline-arrow-in-ref": noInlineArrowInRef,
|
|
1844
|
+
"no-invalid-directives-prop": noInvalidDirectivesProp,
|
|
1845
|
+
"no-jsx-spread": noJsxSpread,
|
|
1846
|
+
"no-listen-in-connected-callback": noListenInConnectedCallback,
|
|
1847
|
+
"no-non-component-exports": noNonComponentExports,
|
|
1848
|
+
"no-property-name-start-with-on": noPropertyNameStartWithOn,
|
|
1849
|
+
"no-render-false": noRenderFalse,
|
|
1850
|
+
"no-unnecessary-attribute-name": noUnnecessaryAttributeName,
|
|
1851
|
+
"no-unnecessary-bind-this": noUnnecessaryBindThis,
|
|
1852
|
+
"no-unnecessary-key": noUnnecessaryKey,
|
|
1853
|
+
"tag-name-rules": tagNameRules
|
|
1854
|
+
});
|
|
5
1855
|
export {
|
|
6
|
-
|
|
1856
|
+
luminaPlugin as default
|
|
7
1857
|
};
|