@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
|
@@ -1,111 +1,164 @@
|
|
|
1
|
-
import { m as makeEslintPlugin
|
|
1
|
+
import { m as makeEslintPlugin } from "../../makePlugin-GmKey29v.js";
|
|
2
|
+
import { TSESTree, AST_TOKEN_TYPES, AST_NODE_TYPES, TSESLint } from "@typescript-eslint/utils";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import { resolve } from "path/posix";
|
|
3
|
-
import
|
|
5
|
+
import globals from "globals";
|
|
6
|
+
import { uncapitalize, kebabToPascal } from "@arcgis/toolkit/string";
|
|
7
|
+
import { e as extractDeclareElementsInterface, g as getComponentDeclaration } from "../../estree-CDc-die8.js";
|
|
4
8
|
const plugin = makeEslintPlugin(
|
|
5
9
|
"webgis",
|
|
6
10
|
(rule) => `https://devtopia.esri.com/WebGIS/arcgis-web-components/tree/main/packages/support-packages/eslint-config/src/plugins/webgis/rules/${rule}.ts`
|
|
7
11
|
);
|
|
8
12
|
const isTestFile = (filePath) => filePath.includes("/test") || filePath.includes(".test") || filePath.includes(".spec") || filePath.includes("e2e") || filePath.includes("__") || filePath.includes("/.");
|
|
9
13
|
const isStorybookFile = (filePath) => filePath.includes(".stories");
|
|
10
|
-
|
|
14
|
+
function hasPublicJsDocTag(sourceCode, node) {
|
|
15
|
+
const jsDocOwner = node.decorators?.at(0) ?? (node.parent?.type === TSESTree.AST_NODE_TYPES.ExportNamedDeclaration || node.parent?.type === TSESTree.AST_NODE_TYPES.ExportDefaultDeclaration ? node.parent : void 0) ?? node;
|
|
16
|
+
const commentTarget = jsDocOwner;
|
|
17
|
+
if (commentTarget === void 0) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return sourceCode.getCommentsBefore(commentTarget).some((comment) => comment.type === AST_TOKEN_TYPES.Block && sourceCode.getText(comment).includes("* @public"));
|
|
21
|
+
}
|
|
11
22
|
plugin.createRule({
|
|
12
|
-
name: "
|
|
23
|
+
name: "consistent-constructor-arguments",
|
|
13
24
|
meta: {
|
|
14
25
|
docs: {
|
|
15
|
-
description:
|
|
16
|
-
|
|
26
|
+
description: "Consistent constructor arguments for Accessor-based classes.",
|
|
27
|
+
// Disabled by default because the rule is most relevant for autocasting
|
|
28
|
+
// and only on public classes. We are discouraging autocasting in new
|
|
29
|
+
// code.
|
|
30
|
+
defaultLevel: "off"
|
|
17
31
|
},
|
|
18
32
|
messages: {
|
|
19
|
-
|
|
33
|
+
name: `By convention, the first parameter on an Accessor-based class must be called "properties". This is needed for autocasting - https://webgis.esri.com/references/api-extractor/api-node-kinds#constructor. If your class has constructors overloads, at least one of them must have a "properties" parameter.`,
|
|
34
|
+
type: `By convention, the constructor argument type of Accessor-based classes must be called \`{{ expectedName }}\` (found \`{{ actualName }}\` instead).
|
|
35
|
+
|
|
36
|
+
If you wish to use a loose type like any/object, create a type alias \`type {{ expectedName }}\` and assign such loose type to that.`
|
|
20
37
|
},
|
|
21
|
-
type: "
|
|
38
|
+
type: "suggestion",
|
|
22
39
|
schema: []
|
|
23
40
|
},
|
|
24
41
|
defaultOptions: [],
|
|
25
42
|
create(context) {
|
|
26
|
-
|
|
27
|
-
const srcIndex = fileName.indexOf("/src/");
|
|
28
|
-
if (srcIndex === -1) {
|
|
29
|
-
return {};
|
|
30
|
-
}
|
|
31
|
-
const basePath = fileName.slice(0, srcIndex + "/src/".length);
|
|
32
|
-
if (isTestFile(fileName) || isStorybookFile(fileName)) {
|
|
43
|
+
if (isTestFile(context.filename)) {
|
|
33
44
|
return {};
|
|
34
45
|
}
|
|
35
46
|
return {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const isRelativeAncestor = specifier.startsWith("../");
|
|
39
|
-
if (!isRelativeAncestor) {
|
|
47
|
+
ClassDeclaration(node) {
|
|
48
|
+
if (!hasPublicJsDocTag(context.sourceCode, node)) {
|
|
40
49
|
return;
|
|
41
50
|
}
|
|
42
|
-
const
|
|
43
|
-
|
|
51
|
+
const hasSubclassDecorator = node.decorators?.some(
|
|
52
|
+
(decorator) => decorator.expression.type === TSESTree.AST_NODE_TYPES.CallExpression && decorator.expression.callee.type === TSESTree.AST_NODE_TYPES.Identifier && decorator.expression.callee.name === "subclass"
|
|
53
|
+
);
|
|
54
|
+
const className = node.id?.name;
|
|
55
|
+
const isTopLevelClass = node.parent?.type === TSESTree.AST_NODE_TYPES.Program || node.parent?.type === TSESTree.AST_NODE_TYPES.ExportNamedDeclaration || node.parent?.type === TSESTree.AST_NODE_TYPES.ExportDefaultDeclaration;
|
|
56
|
+
if (!hasSubclassDecorator || className === void 0 || !isTopLevelClass) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
let firstConstructor;
|
|
60
|
+
let hasPropertiesParameter = false;
|
|
61
|
+
node.body.body.forEach((element) => {
|
|
62
|
+
if (element.type !== TSESTree.AST_NODE_TYPES.MethodDefinition || element.kind !== "constructor") {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
firstConstructor ??= element;
|
|
66
|
+
const firstParameterNode = element.value.params.at(0);
|
|
67
|
+
const firstParameter = firstParameterNode?.type === TSESTree.AST_NODE_TYPES.AssignmentPattern ? firstParameterNode.left : firstParameterNode;
|
|
68
|
+
const isPropertiesParameter = firstParameter?.type === TSESTree.AST_NODE_TYPES.Identifier && firstParameter.name === "properties";
|
|
69
|
+
hasPropertiesParameter ||= isPropertiesParameter;
|
|
70
|
+
if (!isPropertiesParameter) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const typeAnnotation = firstParameter.typeAnnotation?.typeAnnotation;
|
|
74
|
+
const typeAnnotationName = typeAnnotation?.type === TSESTree.AST_NODE_TYPES.TSTypeReference && typeAnnotation.typeName.type === TSESTree.AST_NODE_TYPES.Identifier ? typeAnnotation.typeName.name : void 0;
|
|
75
|
+
const expectedTypeName = `${className}Properties`;
|
|
76
|
+
if (typeAnnotationName === expectedTypeName) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const extendsText = node.superClass ? context.sourceCode.getText(node.superClass) : "";
|
|
80
|
+
const isCollectionClass = extendsText.includes("Collection.ofType") || extendsText.includes("(Collection)") || extendsText === "Collection" || extendsText === "OwningCollection" || context.filename.endsWith(`esri${path.sep}core${path.sep}Collection.ts`);
|
|
81
|
+
if (isCollectionClass) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const typeIsTypeParameter = node.typeParameters?.params.some(
|
|
85
|
+
(parameter) => parameter.name.name === typeAnnotationName
|
|
86
|
+
);
|
|
87
|
+
if (typeIsTypeParameter) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
44
90
|
context.report({
|
|
45
|
-
|
|
46
|
-
|
|
91
|
+
node: typeAnnotation ?? firstParameter,
|
|
92
|
+
messageId: "type",
|
|
93
|
+
data: {
|
|
94
|
+
actualName: typeAnnotation === void 0 ? "<empty>" : context.sourceCode.getText(firstParameter.typeAnnotation?.typeAnnotation),
|
|
95
|
+
expectedName: expectedTypeName
|
|
96
|
+
}
|
|
47
97
|
});
|
|
98
|
+
});
|
|
99
|
+
if (firstConstructor === void 0) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!hasPropertiesParameter) {
|
|
103
|
+
context.report({
|
|
104
|
+
node: firstConstructor,
|
|
105
|
+
messageId: "name"
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
48
108
|
}
|
|
49
109
|
}
|
|
50
110
|
};
|
|
51
111
|
}
|
|
52
112
|
});
|
|
53
|
-
const description$
|
|
113
|
+
const description$6 = `Enforce consistent logging so that ArcGIS developers can easily debug errors or warnings logged by our web components, which may lack a meaningful context in compiled code. See [our documentation on @arcgis/toolkit/log](https://webgis.esri.com/references/toolkit/log).`;
|
|
54
114
|
plugin.createRule({
|
|
55
|
-
name: "
|
|
115
|
+
name: "consistent-logging",
|
|
56
116
|
meta: {
|
|
57
117
|
docs: {
|
|
58
|
-
description: description$
|
|
59
|
-
defaultLevel: "
|
|
118
|
+
description: description$6,
|
|
119
|
+
defaultLevel: "off"
|
|
60
120
|
},
|
|
61
121
|
messages: {
|
|
62
|
-
|
|
122
|
+
consistentLoggingWarning: `For consistency, use the log utility from @arcgis/toolkit/log instead of console.{{logMethodName}}.`
|
|
63
123
|
},
|
|
64
|
-
|
|
65
|
-
|
|
124
|
+
schema: [],
|
|
125
|
+
fixable: void 0,
|
|
126
|
+
type: "suggestion"
|
|
66
127
|
},
|
|
67
128
|
defaultOptions: [],
|
|
68
129
|
create(context) {
|
|
130
|
+
if (isTestFile(context.filename) || isStorybookFile(context.filename)) {
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
69
133
|
return {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return void 0;
|
|
75
|
-
}
|
|
76
|
-
const previousJsDocContent = context.sourceCode.text.slice(
|
|
77
|
-
previousJsDocStart + "/**".length,
|
|
78
|
-
match.index - "*/".length
|
|
79
|
-
);
|
|
80
|
-
const containsOtherComments = previousJsDocContent.includes("*/");
|
|
81
|
-
if (containsOtherComments) {
|
|
82
|
-
return void 0;
|
|
83
|
-
}
|
|
134
|
+
// NOTE: CallExpression is not enough. Use MemberExpression to handle callbacks, e.g. .catch(console.error)
|
|
135
|
+
MemberExpression(node) {
|
|
136
|
+
if (node.type === AST_NODE_TYPES.MemberExpression && node.object.type === AST_NODE_TYPES.Identifier && node.object.name === "console" && node.property.type === AST_NODE_TYPES.Identifier) {
|
|
137
|
+
const logMethodName = node.property.name;
|
|
84
138
|
context.report({
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
139
|
+
node,
|
|
140
|
+
messageId: "consistentLoggingWarning",
|
|
141
|
+
data: {
|
|
142
|
+
logMethodName
|
|
89
143
|
}
|
|
90
144
|
});
|
|
91
|
-
|
|
92
|
-
});
|
|
145
|
+
}
|
|
93
146
|
}
|
|
94
147
|
};
|
|
95
148
|
}
|
|
96
149
|
});
|
|
97
|
-
const reTouchingJsDoc = /\*\/\s+\/\*\*/gu;
|
|
98
|
-
const description$4 = `@arcgis/core imports need to end with .js for better compatibility with ESM CDN builds for @arcgis/core and other packages.`;
|
|
99
|
-
const packagesToEnforce = ["@arcgis/core/", "@amcharts/amcharts4/", "@amcharts/amcharts5/"];
|
|
100
150
|
plugin.createRule({
|
|
101
|
-
name: "
|
|
151
|
+
name: "consistent-mixin-syntax",
|
|
102
152
|
meta: {
|
|
103
153
|
docs: {
|
|
104
|
-
description:
|
|
105
|
-
|
|
154
|
+
description: "Enforce consistent mixin pattern. See https://webgis.esri.com/sdk/contributing/core/core/mixins",
|
|
155
|
+
// Enabled by default because the rule targets a very specific pattern
|
|
156
|
+
// and that pattern is tricky to get right without this rule.
|
|
157
|
+
defaultLevel: "error"
|
|
106
158
|
},
|
|
107
159
|
messages: {
|
|
108
|
-
|
|
160
|
+
updateMixinCode: "Mixin typings need to be updated.",
|
|
161
|
+
typeAnnotationRequired: "Explicit type annotation is required."
|
|
109
162
|
},
|
|
110
163
|
type: "problem",
|
|
111
164
|
fixable: "code",
|
|
@@ -113,43 +166,330 @@ plugin.createRule({
|
|
|
113
166
|
},
|
|
114
167
|
defaultOptions: [],
|
|
115
168
|
create(context) {
|
|
116
|
-
|
|
169
|
+
const sourceCode = context.sourceCode.getText();
|
|
170
|
+
const isMixin = sourceCode.includes("TBase");
|
|
171
|
+
if (!isMixin) {
|
|
117
172
|
return {};
|
|
118
173
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
174
|
+
return {
|
|
175
|
+
VariableDeclarator(node) {
|
|
176
|
+
if (node.id.type !== AST_NODE_TYPES.Identifier || node.init?.type !== AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const body = node.init.body;
|
|
180
|
+
if (body.type !== AST_NODE_TYPES.BlockStatement) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const firstTypeParameter = node.init.typeParameters?.params[0];
|
|
184
|
+
if (firstTypeParameter?.name.name !== "TBase") {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
let baseName = node.id.name;
|
|
188
|
+
if (baseName.startsWith("Exported")) {
|
|
189
|
+
baseName = baseName.slice("Exported".length);
|
|
190
|
+
}
|
|
191
|
+
if (baseName.endsWith("Mixin")) {
|
|
192
|
+
baseName = baseName.slice(0, -"Mixin".length);
|
|
193
|
+
}
|
|
194
|
+
const restTypeParameters = node.init.typeParameters?.params?.slice(1).map((param) => context.sourceCode.getText(param)).join(", ") ?? "";
|
|
195
|
+
const formattedRestTypeParameters = restTypeParameters.length === 0 ? "" : `<${restTypeParameters}>`;
|
|
196
|
+
const restTypeArguments = node.init.typeParameters?.params?.slice(1).map((param) => context.sourceCode.getText(param.name)).join(", ") ?? "";
|
|
197
|
+
const formattedRestTypeArguments = restTypeArguments.length === 0 ? "" : `<${restTypeArguments}>`;
|
|
198
|
+
{
|
|
199
|
+
const returnTypeCode = `typeof Public${baseName}${formattedRestTypeArguments} & TBase`;
|
|
200
|
+
if (node.init.returnType === void 0) {
|
|
201
|
+
const closeParenToken = context.sourceCode.getFirstToken(node.init, (token) => token.value === ")");
|
|
202
|
+
if (closeParenToken !== null) {
|
|
203
|
+
context.report({
|
|
204
|
+
node: node.init,
|
|
205
|
+
messageId: "updateMixinCode",
|
|
206
|
+
fix: (fixer) => {
|
|
207
|
+
const returnTypeText = `: ${returnTypeCode}`;
|
|
208
|
+
return fixer.insertTextAfter(closeParenToken, returnTypeText);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
} else if (context.sourceCode.getText(node.init.returnType.typeAnnotation) !== returnTypeCode) {
|
|
213
|
+
const typeAnnotation = node.init.returnType.typeAnnotation;
|
|
214
|
+
context.report({
|
|
215
|
+
node: typeAnnotation,
|
|
216
|
+
messageId: "updateMixinCode",
|
|
217
|
+
fix: (fixer) => fixer.replaceText(typeAnnotation, returnTypeCode)
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
let hadMissingTypeError = false;
|
|
222
|
+
let hadTypedBaseVariable = false;
|
|
223
|
+
let privateClassName = baseName;
|
|
224
|
+
let isAbstract = false;
|
|
225
|
+
const members = [];
|
|
226
|
+
for (const statement of body.body) {
|
|
227
|
+
if (statement.type === AST_NODE_TYPES.VariableDeclaration && statement.declarations.length === 1 && statement.declarations[0].id.type === AST_NODE_TYPES.Identifier && statement.declarations[0].id.name === "TypedBase" && statement.declarations[0].init?.type === AST_NODE_TYPES.TSAsExpression) {
|
|
228
|
+
hadTypedBaseVariable = true;
|
|
229
|
+
} else if (statement.type === AST_NODE_TYPES.TSInterfaceDeclaration) {
|
|
230
|
+
for (const member of statement.body.body) {
|
|
231
|
+
members.push(context.sourceCode.getText(member));
|
|
232
|
+
}
|
|
233
|
+
} else if (statement.type === AST_NODE_TYPES.ClassDeclaration) {
|
|
234
|
+
privateClassName = statement.id.name;
|
|
235
|
+
isAbstract = statement.abstract;
|
|
236
|
+
{
|
|
237
|
+
const extendsPart = statement.superClass;
|
|
238
|
+
if (extendsPart === null) {
|
|
239
|
+
context.report({
|
|
240
|
+
node: statement,
|
|
241
|
+
messageId: "updateMixinCode",
|
|
242
|
+
fix: (fixer) => fixer.insertTextAfter(statement.id, ` extends TypedBase`)
|
|
243
|
+
});
|
|
244
|
+
} else if (context.sourceCode.getText(extendsPart) !== "TypedBase") {
|
|
245
|
+
context.report({
|
|
246
|
+
node: statement,
|
|
247
|
+
messageId: "updateMixinCode",
|
|
248
|
+
fix: (fixer) => fixer.replaceText(extendsPart, "TypedBase")
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
for (const member of statement.body.body) {
|
|
253
|
+
const isAbstractMethod = member.type === AST_NODE_TYPES.TSAbstractMethodDefinition;
|
|
254
|
+
const isAbstractProperty = member.type === AST_NODE_TYPES.TSAbstractPropertyDefinition;
|
|
255
|
+
if (member.type === AST_NODE_TYPES.MethodDefinition || isAbstractMethod) {
|
|
256
|
+
if (member.key.type === AST_NODE_TYPES.PrivateIdentifier) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const memberName = context.sourceCode.getText(member.key);
|
|
260
|
+
const formattedMemberName = member.computed ? `[${memberName}]` : memberName;
|
|
261
|
+
if (memberName === "constructor") {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const modifiers = `${member.static ? "static " : ""}${member.accessibility ? `${member.accessibility} ` : ""}${isAbstractMethod ? "abstract " : ""}`;
|
|
265
|
+
const kindName = member.kind === "get" || member.kind === "set" ? `${member.kind} ` : "";
|
|
266
|
+
const functionString = functionToTypeString(
|
|
267
|
+
member.value,
|
|
268
|
+
context,
|
|
269
|
+
member.kind === "set" ? "set" : void 0
|
|
270
|
+
);
|
|
271
|
+
if (functionString === void 0) {
|
|
272
|
+
hadMissingTypeError = true;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
members.push(`${modifiers}${kindName}${formattedMemberName}${functionString};`);
|
|
276
|
+
} else if (member.type === AST_NODE_TYPES.PropertyDefinition || isAbstractProperty) {
|
|
277
|
+
if (member.key.type === AST_NODE_TYPES.PrivateIdentifier) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const memberName = context.sourceCode.getText(member.key);
|
|
281
|
+
const formattedMemberName = member.computed ? `[${memberName}]` : memberName;
|
|
282
|
+
const typeAnnotation = member.typeAnnotation?.typeAnnotation;
|
|
283
|
+
let type = typeAnnotation ? context.sourceCode.getText(typeAnnotation) : member.value === null ? void 0 : expressionToTypeString(member.value, context, false);
|
|
284
|
+
if (type === void 0) {
|
|
285
|
+
type = "any";
|
|
286
|
+
context.report({
|
|
287
|
+
node: member,
|
|
288
|
+
messageId: "typeAnnotationRequired"
|
|
289
|
+
});
|
|
290
|
+
hadMissingTypeError = true;
|
|
291
|
+
}
|
|
292
|
+
const modifiers = `${member.static ? "static " : ""}${member.accessibility ? `${member.accessibility} ` : ""}${isAbstractProperty ? "abstract " : ""}${member.readonly ? "readonly " : ""}`;
|
|
293
|
+
const optionalText = member.optional ? "?" : "";
|
|
294
|
+
members.push(`${modifiers}${formattedMemberName}${optionalText}: ${type};`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} else if (statement.type === AST_NODE_TYPES.ReturnStatement) {
|
|
298
|
+
const returnText = context.sourceCode.getText(statement);
|
|
299
|
+
const expectedReturnText = `return ${privateClassName} as typeof Public${baseName}${formattedRestTypeArguments} & TBase;`;
|
|
300
|
+
if (returnText.replaceAll(reWhiteSpaceOrComma, "") !== expectedReturnText.replaceAll(reWhiteSpaceOrComma, "")) {
|
|
301
|
+
context.report({
|
|
302
|
+
node: statement,
|
|
303
|
+
messageId: "updateMixinCode",
|
|
304
|
+
fix: (fixer) => fixer.replaceText(statement, expectedReturnText)
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const typeConstraint = firstTypeParameter.constraint;
|
|
310
|
+
const typeConstraintText = typeConstraint === void 0 ? "Constructor<Accessor>" : context.sourceCode.getText(typeConstraint);
|
|
311
|
+
if (typeConstraint !== void 0) {
|
|
312
|
+
const typeParameterText = context.sourceCode.getText(firstTypeParameter);
|
|
313
|
+
context.report({
|
|
314
|
+
node: typeConstraint,
|
|
315
|
+
messageId: "updateMixinCode",
|
|
316
|
+
fix: (fixer) => fixer.replaceText(firstTypeParameter, typeParameterText.split("extends")[0])
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
if (!hadTypedBaseVariable) {
|
|
320
|
+
context.report({
|
|
321
|
+
loc: body.loc.start,
|
|
322
|
+
messageId: "updateMixinCode",
|
|
323
|
+
fix: (fixer) => fixer.insertTextAfterRange(
|
|
324
|
+
[
|
|
325
|
+
context.sourceCode.getIndexFromLoc(body.loc.start) + 1,
|
|
326
|
+
context.sourceCode.getIndexFromLoc(body.loc.start) + 1
|
|
327
|
+
],
|
|
328
|
+
`
|
|
329
|
+
const TypedBase = Base as unknown as ${typeConstraintText};`
|
|
330
|
+
)
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
if (hadMissingTypeError) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const localRegionStart = `${regionStart} ${baseName}`;
|
|
337
|
+
const abstractString = isAbstract ? "abstract " : "";
|
|
338
|
+
const autoGeneratedCode = `${localRegionStart}
|
|
339
|
+
declare ${abstractString}class Public${baseName}${formattedRestTypeParameters} {
|
|
340
|
+
constructor(...args: any[]);
|
|
341
|
+
${members.map((member) => ` ${member}`).join("\n")}
|
|
342
|
+
}
|
|
343
|
+
${regionEnd}`;
|
|
344
|
+
const regionStartIndex = sourceCode.indexOf(localRegionStart);
|
|
345
|
+
if (regionStartIndex === -1) {
|
|
346
|
+
context.report({
|
|
347
|
+
loc: node.loc.end,
|
|
348
|
+
messageId: "updateMixinCode",
|
|
349
|
+
fix: (fixer) => fixer.insertTextAfter(node, `
|
|
350
|
+
|
|
351
|
+
${autoGeneratedCode}
|
|
352
|
+
`)
|
|
353
|
+
});
|
|
354
|
+
} else {
|
|
355
|
+
let regionEndString = regionEnd;
|
|
356
|
+
let regionEndIndex = sourceCode.indexOf(regionEndString, regionStartIndex);
|
|
357
|
+
if (regionEndIndex === -1) {
|
|
358
|
+
regionEndString = regionEndCrlf;
|
|
359
|
+
regionEndIndex = sourceCode.indexOf(regionEndString, regionStartIndex);
|
|
360
|
+
}
|
|
361
|
+
if (regionEndIndex === -1) {
|
|
362
|
+
regionEndIndex = sourceCode.length;
|
|
363
|
+
} else {
|
|
364
|
+
regionEndIndex += regionEndString.length;
|
|
365
|
+
}
|
|
366
|
+
const oldRegionCode = sourceCode.slice(regionStartIndex, regionEndIndex);
|
|
367
|
+
const oldRegionCodeWithoutWhitespace = oldRegionCode.replaceAll(reWhiteSpaceOrComma, "");
|
|
368
|
+
const newRegionCodeWithoutWhitespace = autoGeneratedCode.replaceAll(reWhiteSpaceOrComma, "");
|
|
369
|
+
if (oldRegionCodeWithoutWhitespace === newRegionCodeWithoutWhitespace) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const line = sourceCode.slice(0, regionStartIndex).split("\n").length;
|
|
373
|
+
context.report({
|
|
374
|
+
// Region start:
|
|
375
|
+
loc: {
|
|
376
|
+
line,
|
|
377
|
+
column: 0
|
|
378
|
+
},
|
|
379
|
+
messageId: "updateMixinCode",
|
|
380
|
+
fix: (fixer) => fixer.replaceTextRange([regionStartIndex, regionEndIndex], autoGeneratedCode)
|
|
381
|
+
});
|
|
382
|
+
}
|
|
129
383
|
}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
const reWhiteSpaceOrComma = /[\s,]+/gu;
|
|
388
|
+
const regionStart = "// #region Autogenerated";
|
|
389
|
+
const regionEnd = "// #endregion\n";
|
|
390
|
+
const regionEndCrlf = "// #endregion\r\n";
|
|
391
|
+
function functionToTypeString(definition, context, kind = void 0) {
|
|
392
|
+
let hadMissingTypeError = false;
|
|
393
|
+
const parameters = definition.params.map((param, index) => {
|
|
394
|
+
if (param.type === AST_NODE_TYPES.TSParameterProperty) {
|
|
395
|
+
throw new Error("Unsupported");
|
|
396
|
+
}
|
|
397
|
+
const name = param.type === AST_NODE_TYPES.RestElement && param.argument.type === AST_NODE_TYPES.Identifier ? `...${param.argument.name}` : param.type === AST_NODE_TYPES.Identifier ? param.name : param.type === AST_NODE_TYPES.AssignmentPattern && param.left.type === AST_NODE_TYPES.Identifier ? param.left.name : `_arg${index}`;
|
|
398
|
+
const optional = "optional" in param && param.optional || param.type === AST_NODE_TYPES.AssignmentPattern ? "?" : "";
|
|
399
|
+
const typeAnnotation = param.type === AST_NODE_TYPES.AssignmentPattern ? param?.left.typeAnnotation?.typeAnnotation : param.typeAnnotation?.typeAnnotation;
|
|
400
|
+
let type = typeAnnotation === void 0 ? param.type === AST_NODE_TYPES.AssignmentPattern ? expressionToTypeString(param.right, context, false) : void 0 : context.sourceCode.getText(typeAnnotation);
|
|
401
|
+
if (type === void 0) {
|
|
402
|
+
type = "any";
|
|
130
403
|
context.report({
|
|
131
|
-
node:
|
|
132
|
-
messageId: "
|
|
133
|
-
fix: (fixer) => fixer.replaceText(node.source, `"${specifier}.js"`)
|
|
404
|
+
node: param,
|
|
405
|
+
messageId: "typeAnnotationRequired"
|
|
134
406
|
});
|
|
407
|
+
hadMissingTypeError = true;
|
|
135
408
|
}
|
|
136
|
-
return {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
409
|
+
return `${name}${optional}: ${type}`;
|
|
410
|
+
}).join(", ");
|
|
411
|
+
if (hadMissingTypeError) {
|
|
412
|
+
return;
|
|
140
413
|
}
|
|
141
|
-
|
|
142
|
-
|
|
414
|
+
const returnType = kind === "set" ? "" : definition.returnType?.typeAnnotation === void 0 ? definition.body?.type === AST_NODE_TYPES.BlockStatement && definition.body.body.length === 1 && definition.body.body[0]?.type === AST_NODE_TYPES.ReturnStatement && definition.body.body[0].argument !== null ? expressionToTypeString(definition.body.body[0].argument, context, false) : void 0 : context.sourceCode.getText(definition.returnType.typeAnnotation);
|
|
415
|
+
if (returnType === void 0) {
|
|
416
|
+
context.report({
|
|
417
|
+
node: definition,
|
|
418
|
+
messageId: "typeAnnotationRequired"
|
|
419
|
+
});
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const returnToken = kind === "signature" ? " => " : ": ";
|
|
423
|
+
const formattedReturnType = returnType === "" ? "" : `${returnToken}${returnType}`;
|
|
424
|
+
const typeArguments = definition.typeParameters === void 0 ? "" : context.sourceCode.getText(definition.typeParameters);
|
|
425
|
+
return `${typeArguments}(${parameters})${formattedReturnType}`;
|
|
426
|
+
}
|
|
427
|
+
function expressionToTypeString(expression, context, asConst) {
|
|
428
|
+
const text = context.sourceCode.getText(expression);
|
|
429
|
+
if (text === "undefined") {
|
|
430
|
+
return text;
|
|
431
|
+
}
|
|
432
|
+
if (text === "true" || text === "false") {
|
|
433
|
+
return asConst ? text : "boolean";
|
|
434
|
+
}
|
|
435
|
+
if (expression.type === AST_NODE_TYPES.Literal) {
|
|
436
|
+
return asConst ? typeof expression.value === "string" ? JSON.stringify(expression.value) : expression.value?.toString() ?? "null" : typeof expression.value === "string" ? "string" : typeof expression.value === "number" ? "number" : typeof expression.value === "bigint" ? "bigint" : typeof expression.value === "boolean" ? "boolean" : typeof expression.value === "object" && expression.value !== null ? "RegExp" : "null";
|
|
437
|
+
}
|
|
438
|
+
if (expression.type === AST_NODE_TYPES.UnaryExpression && expression.operator === "-" && expression.argument.type === AST_NODE_TYPES.Literal) {
|
|
439
|
+
const value = expression.argument.value;
|
|
440
|
+
if (typeof value === "number" || typeof value === "bigint") {
|
|
441
|
+
return asConst ? `-${value}` : "number";
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (expression.type === AST_NODE_TYPES.TemplateLiteral) {
|
|
445
|
+
return "string";
|
|
446
|
+
}
|
|
447
|
+
if (expression.type === AST_NODE_TYPES.TSAsExpression || expression.type === AST_NODE_TYPES.TSTypeAssertion) {
|
|
448
|
+
const typeAnnotation = expression.typeAnnotation;
|
|
449
|
+
if (typeAnnotation.type === AST_NODE_TYPES.TSTypeReference && typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier) {
|
|
450
|
+
if (typeAnnotation.typeName.name === "const") {
|
|
451
|
+
return expressionToTypeString(expression.expression, context, true);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return context.sourceCode.getText(typeAnnotation);
|
|
455
|
+
}
|
|
456
|
+
if (expression.type === AST_NODE_TYPES.TSSatisfiesExpression) {
|
|
457
|
+
const typeAnnotation = expression.typeAnnotation;
|
|
458
|
+
return asConst ? expressionToTypeString(expression.expression, context, true) : context.sourceCode.getText(typeAnnotation);
|
|
459
|
+
}
|
|
460
|
+
if (expression.type === AST_NODE_TYPES.TSNonNullExpression) {
|
|
461
|
+
return expressionToTypeString(expression.expression, context, asConst);
|
|
462
|
+
}
|
|
463
|
+
if (expression.type === AST_NODE_TYPES.NewExpression && expression.callee.type === AST_NODE_TYPES.Identifier) {
|
|
464
|
+
return `${expression.callee.name}${expression.typeArguments ? context.sourceCode.getText(expression.typeArguments) : ""}`;
|
|
465
|
+
}
|
|
466
|
+
if (expression.type === AST_NODE_TYPES.ConditionalExpression) {
|
|
467
|
+
const left = expressionToTypeString(expression.consequent, context, asConst);
|
|
468
|
+
const right = expressionToTypeString(expression.alternate, context, asConst);
|
|
469
|
+
if (left === void 0 || right === void 0) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
return left === right ? left : `${left} | ${right}`;
|
|
473
|
+
}
|
|
474
|
+
if (expression.type === AST_NODE_TYPES.Identifier) {
|
|
475
|
+
return `typeof ${expression.name}`;
|
|
476
|
+
}
|
|
477
|
+
if (expression.type === AST_NODE_TYPES.FunctionExpression || expression.type === AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
478
|
+
return functionToTypeString(expression, context, "signature");
|
|
479
|
+
}
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const description$5 = "Using .d.ts files is discouraged. Prefer .ts files instead, as they are type-checked and not in global scope.";
|
|
143
483
|
const allowedNames = /* @__PURE__ */ new Set(["vite-env.d.ts", "components.d.ts"]);
|
|
144
484
|
plugin.createRule({
|
|
145
485
|
name: "no-dts-files",
|
|
146
486
|
meta: {
|
|
147
487
|
docs: {
|
|
148
|
-
description: description$
|
|
488
|
+
description: description$5,
|
|
149
489
|
defaultLevel: "warn"
|
|
150
490
|
},
|
|
151
491
|
messages: {
|
|
152
|
-
avoidDtsFiles: description$
|
|
492
|
+
avoidDtsFiles: description$5
|
|
153
493
|
},
|
|
154
494
|
type: "suggestion",
|
|
155
495
|
schema: []
|
|
@@ -179,44 +519,315 @@ plugin.createRule({
|
|
|
179
519
|
};
|
|
180
520
|
}
|
|
181
521
|
});
|
|
182
|
-
const description$2 = `Enforce consistent logging so that ArcGIS developers can easily debug errors or warnings logged by our web components, which may lack a meaningful context in compiled code. See [our documentation on @arcgis/toolkit/log](https://webgis.esri.com/references/toolkit/log).`;
|
|
183
522
|
plugin.createRule({
|
|
184
|
-
name: "
|
|
523
|
+
name: "no-empty-catch",
|
|
185
524
|
meta: {
|
|
186
525
|
docs: {
|
|
187
|
-
description:
|
|
526
|
+
description: "Disallow empty catch calls since empty catch calls in a promise chain don't catch any exceptions",
|
|
527
|
+
// Enabled by default because this caught real bugs and has autofix
|
|
528
|
+
defaultLevel: "error"
|
|
529
|
+
},
|
|
530
|
+
messages: { emptyCatch: "no empty catch calls" },
|
|
531
|
+
schema: [],
|
|
532
|
+
type: "problem",
|
|
533
|
+
fixable: "code"
|
|
534
|
+
},
|
|
535
|
+
defaultOptions: [],
|
|
536
|
+
create(context) {
|
|
537
|
+
return {
|
|
538
|
+
"CallExpression[callee.property.name='catch'][arguments.length=0]"(node) {
|
|
539
|
+
const [, rangeEnd] = node.range;
|
|
540
|
+
context.report({
|
|
541
|
+
node,
|
|
542
|
+
messageId: "emptyCatch",
|
|
543
|
+
fix: (fixer) => fixer.insertTextBeforeRange([rangeEnd - 1, rangeEnd], "() => {}")
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
const description$4 = `Imports of files outside the src/ folder are not-portable and likely to break for consumers of this package.`;
|
|
550
|
+
plugin.createRule({
|
|
551
|
+
name: "no-import-outside-src",
|
|
552
|
+
meta: {
|
|
553
|
+
docs: {
|
|
554
|
+
description: description$4,
|
|
555
|
+
defaultLevel: "error"
|
|
556
|
+
},
|
|
557
|
+
messages: {
|
|
558
|
+
noImportOutsideSrc: description$4
|
|
559
|
+
},
|
|
560
|
+
type: "problem",
|
|
561
|
+
schema: []
|
|
562
|
+
},
|
|
563
|
+
defaultOptions: [],
|
|
564
|
+
create(context) {
|
|
565
|
+
const fileName = context.filename;
|
|
566
|
+
const srcIndex = fileName.indexOf("/src/");
|
|
567
|
+
if (srcIndex === -1) {
|
|
568
|
+
return {};
|
|
569
|
+
}
|
|
570
|
+
const basePath = fileName.slice(0, srcIndex + "/src/".length);
|
|
571
|
+
if (isTestFile(fileName) || isStorybookFile(fileName)) {
|
|
572
|
+
return {};
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
ImportDeclaration(node) {
|
|
576
|
+
const specifier = node.source.value;
|
|
577
|
+
const isRelativeAncestor = specifier.startsWith("../");
|
|
578
|
+
if (!isRelativeAncestor) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const relativePath = resolve(fileName, "..", specifier);
|
|
582
|
+
if (!relativePath.startsWith(basePath) && !relativePath.endsWith(".json")) {
|
|
583
|
+
context.report({
|
|
584
|
+
messageId: "noImportOutsideSrc",
|
|
585
|
+
node: node.source
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
plugin.createRule({
|
|
593
|
+
name: "no-instanceof",
|
|
594
|
+
meta: {
|
|
595
|
+
docs: {
|
|
596
|
+
description: "Disallow `instanceof` outside an allowlist.",
|
|
597
|
+
// Disabling by default because this matters for
|
|
598
|
+
// lower-level libraries more than higher-level libraries or applications.
|
|
599
|
+
// Also, many false-positives.
|
|
188
600
|
defaultLevel: "off"
|
|
189
|
-
// NOTE: this is turned on conditionally in root eslint config
|
|
190
601
|
},
|
|
602
|
+
schema: [
|
|
603
|
+
{
|
|
604
|
+
type: "object",
|
|
605
|
+
properties: {
|
|
606
|
+
allow: { type: "array", items: { type: "string" }, uniqueItems: true }
|
|
607
|
+
},
|
|
608
|
+
additionalProperties: false
|
|
609
|
+
}
|
|
610
|
+
],
|
|
191
611
|
messages: {
|
|
192
|
-
|
|
612
|
+
instanceOf: "Avoid `instanceof` because it statically pulls the module defining the constructor into the bundle. Instead, use a Symbol together with a type guard function."
|
|
613
|
+
},
|
|
614
|
+
type: "problem"
|
|
615
|
+
},
|
|
616
|
+
defaultOptions: [{}],
|
|
617
|
+
create(context) {
|
|
618
|
+
const options = normalizeOptions(context.options[0]);
|
|
619
|
+
const sourceCode = context.sourceCode;
|
|
620
|
+
return {
|
|
621
|
+
"BinaryExpression[operator='instanceof']"(node) {
|
|
622
|
+
const right = node.right;
|
|
623
|
+
if (right.type === AST_NODE_TYPES.ThisExpression) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (right.type !== AST_NODE_TYPES.Identifier) {
|
|
627
|
+
context.report({ node: right, messageId: "instanceOf" });
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const scope = sourceCode.getScope(node);
|
|
631
|
+
if (!isAllowedIdentifier(right.name, scope, options)) {
|
|
632
|
+
context.report({ node: right, messageId: "instanceOf" });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
function normalizeOptions(options) {
|
|
639
|
+
return {
|
|
640
|
+
allowSet: new Set(options?.allow ?? [])
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function resolveVariable(scope, name) {
|
|
644
|
+
for (let current = scope; current; current = current.upper) {
|
|
645
|
+
const variable = current.variables.find((entry) => entry.name === name);
|
|
646
|
+
if (variable) {
|
|
647
|
+
return variable;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const builtInTypes = new Set(Object.keys({ ...globals.browser, ...globals.builtin }));
|
|
653
|
+
function isAllowedIdentifier(name, scope, options) {
|
|
654
|
+
if (options.allowSet.has(name)) {
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
const variable = resolveVariable(scope, name);
|
|
658
|
+
if (builtInTypes.has(name) && isUnshadowedBuiltIn(variable)) {
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
return isClassVariable(variable);
|
|
662
|
+
}
|
|
663
|
+
function isUnshadowedBuiltIn(variable) {
|
|
664
|
+
if (!variable) {
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
return variable.scope.type === TSESLint.Scope.ScopeType.global && variable.defs.length === 0;
|
|
668
|
+
}
|
|
669
|
+
function isClassVariable(variable) {
|
|
670
|
+
return variable?.defs.some((definition) => isClassDefinition(definition)) ?? false;
|
|
671
|
+
}
|
|
672
|
+
function isClassDefinition(definition) {
|
|
673
|
+
const definitionType = definition.type;
|
|
674
|
+
if (definitionType === TSESLint.Scope.DefinitionType.ClassName) {
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
if (definitionType !== TSESLint.Scope.DefinitionType.Variable) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
const variableNode = definition.node;
|
|
681
|
+
if (variableNode?.type !== AST_NODE_TYPES.VariableDeclarator) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
return variableNode.init?.type === AST_NODE_TYPES.ClassExpression;
|
|
685
|
+
}
|
|
686
|
+
plugin.createRule({
|
|
687
|
+
name: "no-kebab-case-props",
|
|
688
|
+
meta: {
|
|
689
|
+
docs: {
|
|
690
|
+
description: "Disallow kebab-case props on custom JSX elements.",
|
|
691
|
+
// Off by default till
|
|
692
|
+
// https://devtopia.esri.com/WebGIS/arcgis-web-components/issues/5364#issuecomment-6321339
|
|
693
|
+
// is resolved
|
|
694
|
+
defaultLevel: "off"
|
|
695
|
+
},
|
|
696
|
+
messages: {
|
|
697
|
+
kebab: "JSX props should use camelCase instead of kebab-case, except for data- or aria- attributes"
|
|
193
698
|
},
|
|
194
699
|
schema: [],
|
|
195
|
-
|
|
196
|
-
|
|
700
|
+
type: "problem",
|
|
701
|
+
fixable: "code"
|
|
197
702
|
},
|
|
198
703
|
defaultOptions: [],
|
|
199
704
|
create(context) {
|
|
200
|
-
|
|
705
|
+
return {
|
|
706
|
+
// looks for JSXAttributes that have a hyphen in their name
|
|
707
|
+
"JSXAttribute[name.name=/-/]"(node) {
|
|
708
|
+
const attrName = node.name.type === AST_NODE_TYPES.JSXIdentifier ? node.name.name : null;
|
|
709
|
+
if (!attrName || attrName.startsWith("aria-") || attrName.startsWith("data-")) {
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (node.parent.name.type === AST_NODE_TYPES.JSXIdentifier) {
|
|
713
|
+
const tagName = node.parent.name.name;
|
|
714
|
+
if (isNativeTagName(tagName)) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
context.report({
|
|
719
|
+
node,
|
|
720
|
+
messageId: "kebab",
|
|
721
|
+
fix: (fixer) => fixer.replaceText(node.name, uncapitalize(kebabToPascal(attrName)))
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
function isNativeTagName(tagName) {
|
|
728
|
+
const normalizedTagName = tagName.toLowerCase();
|
|
729
|
+
const isCapitalized = /^[A-Z]/u.test(tagName);
|
|
730
|
+
if (isCapitalized) {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
return !normalizedTagName.includes("-") || blockListedCustomElementNames.has(normalizedTagName);
|
|
734
|
+
}
|
|
735
|
+
const blockListedCustomElementNames = /* @__PURE__ */ new Set([
|
|
736
|
+
"annotation-xml",
|
|
737
|
+
"color-profile",
|
|
738
|
+
"font-face",
|
|
739
|
+
"font-face-src",
|
|
740
|
+
"font-face-uri",
|
|
741
|
+
"font-face-format",
|
|
742
|
+
"font-face-name",
|
|
743
|
+
"missing-glyph"
|
|
744
|
+
]);
|
|
745
|
+
const description$3 = "In Storybook render functions, let `args` infer from `Meta<T>` or `StoryObj<T>` instead of annotating the render parameter.";
|
|
746
|
+
plugin.createRule({
|
|
747
|
+
name: "no-story-render-args-type-annotation",
|
|
748
|
+
meta: {
|
|
749
|
+
docs: {
|
|
750
|
+
description: description$3,
|
|
751
|
+
defaultLevel: "off"
|
|
752
|
+
},
|
|
753
|
+
messages: {
|
|
754
|
+
noStoryRenderArgsTypeAnnotation: "Do not annotate `render(args)` in stories. Type the story with `Meta<T>` or `StoryObj<T>` and let `args` infer from that."
|
|
755
|
+
},
|
|
756
|
+
schema: [],
|
|
757
|
+
type: "suggestion",
|
|
758
|
+
fixable: "code"
|
|
759
|
+
},
|
|
760
|
+
defaultOptions: [],
|
|
761
|
+
create(context) {
|
|
762
|
+
if (!isStorybookFile(context.filename)) {
|
|
201
763
|
return {};
|
|
202
764
|
}
|
|
203
765
|
return {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
766
|
+
Property(node) {
|
|
767
|
+
if (!isRenderProperty(node)) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const [firstParameter] = node.value.params;
|
|
771
|
+
if (firstParameter?.type !== AST_NODE_TYPES.Identifier || firstParameter.typeAnnotation === void 0) {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
context.report({
|
|
775
|
+
node: firstParameter.typeAnnotation,
|
|
776
|
+
messageId: "noStoryRenderArgsTypeAnnotation",
|
|
777
|
+
fix: (fixer) => fixer.remove(firstParameter.typeAnnotation)
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
function isRenderProperty(node) {
|
|
784
|
+
return node.key.type === AST_NODE_TYPES.Identifier && node.key.name === "render" && (node.value.type === AST_NODE_TYPES.ArrowFunctionExpression || node.value.type === AST_NODE_TYPES.FunctionExpression);
|
|
785
|
+
}
|
|
786
|
+
const description$2 = `Having two JSDoc comments next to each other is most likely a mistake - consider combining them into one, or separating them for clarity.`;
|
|
787
|
+
plugin.createRule({
|
|
788
|
+
name: "no-touching-jsdoc",
|
|
789
|
+
meta: {
|
|
790
|
+
docs: {
|
|
791
|
+
description: description$2,
|
|
792
|
+
defaultLevel: "warn"
|
|
793
|
+
},
|
|
794
|
+
messages: {
|
|
795
|
+
noTouchingJsDoc: description$2
|
|
796
|
+
},
|
|
797
|
+
type: "problem",
|
|
798
|
+
schema: []
|
|
799
|
+
},
|
|
800
|
+
defaultOptions: [],
|
|
801
|
+
create(context) {
|
|
802
|
+
return {
|
|
803
|
+
Program() {
|
|
804
|
+
Array.from(context.sourceCode.text.matchAll(reTouchingJsDoc), (match) => {
|
|
805
|
+
const previousJsDocStart = context.sourceCode.text.slice(0, match.index).lastIndexOf("/**");
|
|
806
|
+
if (previousJsDocStart === -1) {
|
|
807
|
+
return void 0;
|
|
808
|
+
}
|
|
809
|
+
const previousJsDocContent = context.sourceCode.text.slice(
|
|
810
|
+
previousJsDocStart + "/**".length,
|
|
811
|
+
match.index - "*/".length
|
|
812
|
+
);
|
|
813
|
+
const containsOtherComments = previousJsDocContent.includes("*/");
|
|
814
|
+
if (containsOtherComments) {
|
|
815
|
+
return void 0;
|
|
816
|
+
}
|
|
208
817
|
context.report({
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
818
|
+
messageId: "noTouchingJsDoc",
|
|
819
|
+
loc: {
|
|
820
|
+
start: context.sourceCode.getLocFromIndex(match.index),
|
|
821
|
+
end: context.sourceCode.getLocFromIndex(match.index + match[0].length)
|
|
213
822
|
}
|
|
214
823
|
});
|
|
215
|
-
|
|
824
|
+
return void 0;
|
|
825
|
+
});
|
|
216
826
|
}
|
|
217
827
|
};
|
|
218
828
|
}
|
|
219
829
|
});
|
|
830
|
+
const reTouchingJsDoc = /\*\/\s+\/\*\*/gu;
|
|
220
831
|
const description$1 = `Do not use links like [](#something) as they are not portable across .d.ts, and are not validated at build time. Use {@link } syntax, or absolute links. See https://webgis.esri.com/references/api-extractor/tags-reference#link)`;
|
|
221
832
|
plugin.createRule({
|
|
222
833
|
name: "no-unsafe-hash-links",
|
|
@@ -272,47 +883,111 @@ function computeReplacement(declareElementsInterface, anchor) {
|
|
|
272
883
|
const tagName = declaration.key.value;
|
|
273
884
|
return `components/${tagName}#${anchor}`;
|
|
274
885
|
}
|
|
275
|
-
const description = "In Storybook render functions, let `args` infer from `Meta<T>` or `StoryObj<T>` instead of annotating the render parameter.";
|
|
276
886
|
plugin.createRule({
|
|
277
|
-
name: "
|
|
887
|
+
name: "require-await-for-async-screenshot-assert",
|
|
278
888
|
meta: {
|
|
279
889
|
docs: {
|
|
280
|
-
description,
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
messages: {
|
|
284
|
-
noStoryRenderArgsTypeAnnotation: "Do not annotate `render(args)` in stories. Type the story with `Meta<T>` or `StoryObj<T>` and let `args` infer from that."
|
|
890
|
+
description: "Async tests must await a screenshot assertion.",
|
|
891
|
+
// Enabled by default because it targets a specific pattern and has autofix
|
|
892
|
+
defaultLevel: "error"
|
|
285
893
|
},
|
|
894
|
+
messages: { asyncAssert: "Async tests must await a screenshot assertion." },
|
|
286
895
|
schema: [],
|
|
287
|
-
type: "
|
|
896
|
+
type: "problem",
|
|
288
897
|
fixable: "code"
|
|
289
898
|
},
|
|
290
899
|
defaultOptions: [],
|
|
291
900
|
create(context) {
|
|
292
|
-
if (!
|
|
901
|
+
if (!isTestFile(context.filename)) {
|
|
293
902
|
return {};
|
|
294
903
|
}
|
|
295
904
|
return {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
const [firstParameter] = node.value.params;
|
|
301
|
-
if (firstParameter?.type !== AST_NODE_TYPES.Identifier || firstParameter.typeAnnotation === void 0) {
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
905
|
+
"CallExpression[callee.name='test'] > ArrowFunctionExpression[async] ExpressionStatement > CallExpression[callee.object.name='assert'][callee.property.name='screenshot']"(node) {
|
|
906
|
+
const [rangeStart] = node.range;
|
|
304
907
|
context.report({
|
|
305
|
-
node
|
|
306
|
-
messageId: "
|
|
307
|
-
fix: (fixer) => fixer.
|
|
908
|
+
node,
|
|
909
|
+
messageId: "asyncAssert",
|
|
910
|
+
fix: (fixer) => fixer.insertTextBeforeRange([rangeStart - 1, rangeStart], " await")
|
|
308
911
|
});
|
|
309
912
|
}
|
|
310
913
|
};
|
|
311
914
|
}
|
|
312
915
|
});
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
916
|
+
plugin.createRule({
|
|
917
|
+
name: "require-button-type",
|
|
918
|
+
meta: {
|
|
919
|
+
docs: {
|
|
920
|
+
description: "Prevent usage of button elements without an explicit type attribute.",
|
|
921
|
+
// Disabling by default for now because it found many cases (that
|
|
922
|
+
// presumably work correctly today) so enabling it is a bit too disruptive.
|
|
923
|
+
// We should coordinate enabling it with enabling other lint rules to
|
|
924
|
+
// announce at once.
|
|
925
|
+
defaultLevel: "off"
|
|
926
|
+
},
|
|
927
|
+
messages: { missingType: "An explicit type attribute is required on buttons" },
|
|
928
|
+
schema: [],
|
|
929
|
+
type: "problem",
|
|
930
|
+
fixable: "code"
|
|
931
|
+
},
|
|
932
|
+
defaultOptions: [],
|
|
933
|
+
create(context) {
|
|
934
|
+
return {
|
|
935
|
+
"JSXOpeningElement[name.name='button']"(node) {
|
|
936
|
+
if (!node.attributes.some((attr) => attr.type === AST_NODE_TYPES.JSXAttribute && attr.name.name === "type")) {
|
|
937
|
+
context.report({
|
|
938
|
+
node,
|
|
939
|
+
messageId: "missingType",
|
|
940
|
+
fix: (fixer) => fixer.insertTextAfter(node.name, ' type="button"')
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
const description = `@arcgis/core imports need to end with .js for better compatibility with ESM CDN builds for @arcgis/core and other packages.`;
|
|
948
|
+
const packagesToEnforce = ["@arcgis/core/", "@amcharts/amcharts4/", "@amcharts/amcharts5/"];
|
|
949
|
+
plugin.createRule({
|
|
950
|
+
name: "require-js-in-imports",
|
|
951
|
+
meta: {
|
|
952
|
+
docs: {
|
|
953
|
+
description,
|
|
954
|
+
defaultLevel: "warn"
|
|
955
|
+
},
|
|
956
|
+
messages: {
|
|
957
|
+
requireJsInCoreImport: description
|
|
958
|
+
},
|
|
959
|
+
type: "problem",
|
|
960
|
+
fixable: "code",
|
|
961
|
+
schema: []
|
|
962
|
+
},
|
|
963
|
+
defaultOptions: [],
|
|
964
|
+
create(context) {
|
|
965
|
+
if (isTestFile(context.filename)) {
|
|
966
|
+
return {};
|
|
967
|
+
}
|
|
968
|
+
function updateSpecifier(node) {
|
|
969
|
+
if (node.source.type !== AST_NODE_TYPES.Literal) {
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const specifier = node.source.value;
|
|
973
|
+
const lastSlashIndex = specifier?.toString().lastIndexOf("/") ?? 0;
|
|
974
|
+
const dotSearchIndex = lastSlashIndex === -1 ? 0 : lastSlashIndex + 1;
|
|
975
|
+
if (typeof specifier !== "string" || !packagesToEnforce.some((pkg) => specifier.startsWith(pkg)) || // Already ends with .js or .json
|
|
976
|
+
specifier.includes(".", dotSearchIndex)) {
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
context.report({
|
|
980
|
+
node: node.source,
|
|
981
|
+
messageId: "requireJsInCoreImport",
|
|
982
|
+
fix: (fixer) => fixer.replaceText(node.source, `"${specifier}.js"`)
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
ImportDeclaration: updateSpecifier,
|
|
987
|
+
ImportExpression: updateSpecifier
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
});
|
|
316
991
|
const webgisPlugin = plugin.finalize();
|
|
317
992
|
export {
|
|
318
993
|
webgisPlugin
|