@ethisyscore/eslint-plugin-coreconnect 1.71.3 → 1.71.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethisyscore/eslint-plugin-coreconnect",
|
|
3
|
-
"version": "1.71.
|
|
3
|
+
"version": "1.71.4",
|
|
4
4
|
"description": "ESLint rules enforcing EthisysCore plugin frontend conventions. Published so a new rule reaches every plugin on a version bump, rather than being copied into each scaffold and drifting.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -85,9 +85,18 @@ export function looksLikeComponent(init) {
|
|
|
85
85
|
* @param {string} options.messageId
|
|
86
86
|
* @param {string} options.description
|
|
87
87
|
* @param {string} options.message
|
|
88
|
+
* @param {{ source: string, name: string }} options.canonical The promoted component this rule
|
|
89
|
+
* points people at. A local declaration that COMPOSES it is exempt - see below.
|
|
88
90
|
* @returns {import("eslint").Rule.RuleModule}
|
|
89
91
|
*/
|
|
90
|
-
export function createNoLocalComponentRule({ pattern, messageId, description, message }) {
|
|
92
|
+
export function createNoLocalComponentRule({ pattern, messageId, description, message, canonical }) {
|
|
93
|
+
// Fail at construction, not on the first source file that happens to contain an import. Without
|
|
94
|
+
// this a fifth rule added without a `canonical` line throws a TypeError from inside the
|
|
95
|
+
// ImportDeclaration visitor, which surfaces as an opaque lint crash on an arbitrary file.
|
|
96
|
+
if (!canonical?.source || !canonical?.name) {
|
|
97
|
+
throw new Error(`${messageId}: createNoLocalComponentRule needs canonical { source, name }`);
|
|
98
|
+
}
|
|
99
|
+
|
|
91
100
|
return {
|
|
92
101
|
meta: {
|
|
93
102
|
type: "problem",
|
|
@@ -96,7 +105,49 @@ export function createNoLocalComponentRule({ pattern, messageId, description, me
|
|
|
96
105
|
schema: [],
|
|
97
106
|
},
|
|
98
107
|
create(context) {
|
|
99
|
-
const
|
|
108
|
+
const sourceCode = context.sourceCode;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Local names bound to the canonical export, so `import { DateInput as Picker }` still counts.
|
|
112
|
+
*/
|
|
113
|
+
const canonicalNames = new Set();
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The scope variables those names declare.
|
|
117
|
+
*
|
|
118
|
+
* Names alone are not enough. An identifier spelled like the canonical import may resolve to
|
|
119
|
+
* something else entirely - `({ DateInput }) => <DateInput />` uses a destructured PROP - and
|
|
120
|
+
* counting it granted the exemption to a component that composes nothing. That made the rule
|
|
121
|
+
* bypassable by accident: any wrapper taking a prop named after the component would quietly
|
|
122
|
+
* stop being checked. Comparing the resolved variable is what closes it.
|
|
123
|
+
*/
|
|
124
|
+
const canonicalVariables = new Set();
|
|
125
|
+
|
|
126
|
+
/** Source positions where the canonical binding is used. */
|
|
127
|
+
const canonicalUses = [];
|
|
128
|
+
|
|
129
|
+
/** Matching declarations, judged at `Program:exit` once the uses above are known. */
|
|
130
|
+
const candidates = [];
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A WRAPPER, not a re-implementation.
|
|
134
|
+
*
|
|
135
|
+
* The false positive this closes. Each of these rules exists to stop a plugin hand-rolling a
|
|
136
|
+
* primitive that `plugin-ui` already ships, and every one of them documents that importing the
|
|
137
|
+
* promoted component and rendering it is the compliant outcome. But the test is a name suffix,
|
|
138
|
+
* so it also fired on components that do exactly that: `legal`'s `DateField` (a pass-through
|
|
139
|
+
* that maps `error?: string` onto `error` + `helperText`) and `projects`' `PeriodDateInput`
|
|
140
|
+
* (adds commit-on-change), both of which render the SDK's `DateInput`. Two repos hit it the
|
|
141
|
+
* day the rule turned on, and every future wrapper would too.
|
|
142
|
+
*
|
|
143
|
+
* Range containment is what keeps this narrow: only the declaration whose own body reaches the
|
|
144
|
+
* canonical binding is exempt, so a file that legitimately wraps it AND hand-rolls a second
|
|
145
|
+
* one next door still reports the second.
|
|
146
|
+
*/
|
|
147
|
+
const composesCanonical = (node) =>
|
|
148
|
+
canonicalUses.some((at) => at >= node.range[0] && at <= node.range[1]);
|
|
149
|
+
|
|
150
|
+
const check = (idNode, declarationNode) => {
|
|
100
151
|
if (!idNode || idNode.type !== "Identifier") {
|
|
101
152
|
return;
|
|
102
153
|
}
|
|
@@ -104,15 +155,79 @@ export function createNoLocalComponentRule({ pattern, messageId, description, me
|
|
|
104
155
|
return;
|
|
105
156
|
}
|
|
106
157
|
|
|
107
|
-
|
|
158
|
+
candidates.push({ idNode, declarationNode });
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The nearest binding of `name` visible from `scope`.
|
|
163
|
+
*
|
|
164
|
+
* Resolved by walking the scope chain rather than reading the import variable's `references`,
|
|
165
|
+
* because whether a JSX element name counts as a reference depends on the parser: the
|
|
166
|
+
* TypeScript parser's scope manager records one, plain espree does not - which is why
|
|
167
|
+
* `jsx-uses-vars` exists at all. This asks the question the same way under both.
|
|
168
|
+
*/
|
|
169
|
+
const resolveByName = (scope, name) => {
|
|
170
|
+
for (let current = scope; current; current = current.upper) {
|
|
171
|
+
const found = current.variables.find((variable) => variable.name === name);
|
|
172
|
+
if (found) {
|
|
173
|
+
return found;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const noteCanonicalUse = (node) => {
|
|
180
|
+
if (!canonicalNames.has(node.name)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const resolved = resolveByName(sourceCode.getScope(node), node.name);
|
|
185
|
+
if (resolved && !canonicalVariables.has(resolved)) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
canonicalUses.push(node.range[0]);
|
|
108
190
|
};
|
|
109
191
|
|
|
110
192
|
return {
|
|
111
|
-
|
|
112
|
-
|
|
193
|
+
ImportDeclaration: (node) => {
|
|
194
|
+
if (node.source.value !== canonical.source) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const declared = sourceCode.getDeclaredVariables(node);
|
|
198
|
+
for (const specifier of node.specifiers) {
|
|
199
|
+
if (
|
|
200
|
+
specifier.type === "ImportSpecifier" &&
|
|
201
|
+
specifier.imported.type === "Identifier" &&
|
|
202
|
+
specifier.imported.name === canonical.name
|
|
203
|
+
) {
|
|
204
|
+
canonicalNames.add(specifier.local.name);
|
|
205
|
+
const variable = declared.find((candidate) =>
|
|
206
|
+
candidate.defs.some((def) => def.name === specifier.local),
|
|
207
|
+
);
|
|
208
|
+
if (variable) {
|
|
209
|
+
canonicalVariables.add(variable);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
// Both node types are needed: `<DateInput />` is a JSXIdentifier, while `styled(DateInput)`,
|
|
215
|
+
// `React.createElement(DateInput)` and passing it as a prop are plain Identifiers.
|
|
216
|
+
Identifier: noteCanonicalUse,
|
|
217
|
+
JSXIdentifier: noteCanonicalUse,
|
|
218
|
+
FunctionDeclaration: (node) => check(node.id, node),
|
|
219
|
+
ClassDeclaration: (node) => check(node.id, node),
|
|
113
220
|
VariableDeclarator: (node) => {
|
|
114
221
|
if (looksLikeComponent(node.init)) {
|
|
115
|
-
check(node.id);
|
|
222
|
+
check(node.id, node);
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
"Program:exit": () => {
|
|
226
|
+
for (const { idNode, declarationNode } of candidates) {
|
|
227
|
+
if (composesCanonical(declarationNode)) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
context.report({ node: idNode, messageId, data: { name: idNode.name } });
|
|
116
231
|
}
|
|
117
232
|
},
|
|
118
233
|
};
|
|
@@ -22,6 +22,7 @@ export default createNoLocalComponentRule({
|
|
|
22
22
|
// Anchored at the end so `DataGridProps`, `useDataGridState` and a `ProjectsDataGridColumns`
|
|
23
23
|
// helper are left alone; the shared helper's guard keeps hooks out of it.
|
|
24
24
|
pattern: /(PersistentDataGrid|DataGridPagination|GridStatePersistence)$/,
|
|
25
|
+
canonical: { source: "@ethisyscore/plugin-ui/components/data-grid", name: "PersistentDataGrid" },
|
|
25
26
|
messageId: "noLocalDataGrid",
|
|
26
27
|
description:
|
|
27
28
|
"Disallow a locally-declared persistent data grid - import from @ethisyscore/plugin-ui/components/data-grid",
|
|
@@ -23,6 +23,7 @@ export default createNoLocalComponentRule({
|
|
|
23
23
|
// Anchored at the end so `DateInputProps` and `DateRangeFilter` are left alone; the shared helper
|
|
24
24
|
// excludes hooks, so `useDatePicker` and `useDateInputValidation` do not match either.
|
|
25
25
|
pattern: /Date(Time)?(Input|Picker|Field)$/,
|
|
26
|
+
canonical: { source: "@ethisyscore/plugin-ui/components/date", name: "DateInput" },
|
|
26
27
|
messageId: "noLocalDateInput",
|
|
27
28
|
description:
|
|
28
29
|
"Disallow a locally-declared date input - import DateInput from @ethisyscore/plugin-ui/components/date",
|
|
@@ -17,6 +17,7 @@ export default createNoLocalComponentRule({
|
|
|
17
17
|
// Loose enough to catch the names the same panel gets rewritten under, while the shared helper's
|
|
18
18
|
// hook guard keeps `useEmptyState` out of it.
|
|
19
19
|
pattern: /(EmptyState|NoResults|NoData|BlankSlate)$/,
|
|
20
|
+
canonical: { source: "@ethisyscore/plugin-ui/components/shared", name: "EmptyState" },
|
|
20
21
|
messageId: "noLocalEmptyState",
|
|
21
22
|
description:
|
|
22
23
|
"Disallow a locally-declared empty state - import EmptyState from @ethisyscore/plugin-ui/components/shared",
|
|
@@ -19,6 +19,7 @@ export default createNoLocalComponentRule({
|
|
|
19
19
|
// composes the shared input is a legitimate local component; a component that IS the search field
|
|
20
20
|
// is not. Hook names are excluded by the shared helper.
|
|
21
21
|
pattern: /Search(Input|Bar|Field)$/,
|
|
22
|
+
canonical: { source: "@ethisyscore/plugin-ui/components/ui", name: "DebouncedSearchInput" },
|
|
22
23
|
messageId: "noLocalSearchInput",
|
|
23
24
|
description:
|
|
24
25
|
"Disallow a locally-declared search input - import DebouncedSearchInput from @ethisyscore/plugin-ui",
|