pasika 0.10.22 → 0.10.24
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/eslint/pasika/index.js +163 -35
- package/package.json +1 -1
|
@@ -59,6 +59,7 @@ import path from "path";
|
|
|
59
59
|
import ts from "typescript";
|
|
60
60
|
var isPascalCase = (name) => /^[A-Z][A-Za-z0-9]*$/.test(name);
|
|
61
61
|
var isHookCallName = (name) => /^use[A-Z]/.test(name);
|
|
62
|
+
var isHandlerName = (name) => /^handle[A-Z]/.test(name);
|
|
62
63
|
function isExported(node) {
|
|
63
64
|
if (!ts.canHaveModifiers(node)) return false;
|
|
64
65
|
return (ts.getModifiers(node) ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
@@ -87,35 +88,77 @@ function containsJsx(node) {
|
|
|
87
88
|
ts.forEachChild(node, visit);
|
|
88
89
|
return found;
|
|
89
90
|
}
|
|
90
|
-
function
|
|
91
|
+
function containsDataFetch(node) {
|
|
91
92
|
let found = false;
|
|
92
93
|
const visit = (child) => {
|
|
93
94
|
if (found) return;
|
|
94
|
-
if (ts.isCallExpression(child)) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (ts.isIdentifier(expression)) {
|
|
98
|
-
name = expression.text;
|
|
99
|
-
} else if (ts.isPropertyAccessExpression(expression)) {
|
|
100
|
-
name = expression.name.text;
|
|
101
|
-
}
|
|
102
|
-
if (isHookCallName(name)) {
|
|
103
|
-
found = true;
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
95
|
+
if (ts.isCallExpression(child) && ts.isIdentifier(child.expression) && child.expression.text === "zodFetch") {
|
|
96
|
+
found = true;
|
|
97
|
+
return;
|
|
106
98
|
}
|
|
107
99
|
ts.forEachChild(child, visit);
|
|
108
100
|
};
|
|
109
101
|
ts.forEachChild(node, visit);
|
|
110
102
|
return found;
|
|
111
103
|
}
|
|
112
|
-
function
|
|
113
|
-
|
|
104
|
+
function isComponentTagName(tagName2) {
|
|
105
|
+
if (ts.isIdentifier(tagName2)) return isPascalCase(tagName2.text);
|
|
106
|
+
if (ts.isPropertyAccessExpression(tagName2)) {
|
|
107
|
+
let root = tagName2.expression;
|
|
108
|
+
while (ts.isPropertyAccessExpression(root)) root = root.expression;
|
|
109
|
+
return ts.isIdentifier(root) && isPascalCase(root.text);
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
function containsOwnedHandlerProp(node) {
|
|
114
|
+
const handlerNames = /* @__PURE__ */ new Set();
|
|
115
|
+
const collectHandlers = (child) => {
|
|
116
|
+
if (ts.isVariableDeclaration(child) && ts.isIdentifier(child.name) && isHandlerName(child.name.text) && child.initializer && (ts.isArrowFunction(child.initializer) || ts.isFunctionExpression(child.initializer) || ts.isCallExpression(child.initializer) && ts.isPropertyAccessExpression(child.initializer.expression) && child.initializer.expression.name.text === "useCallback" || ts.isCallExpression(child.initializer) && ts.isIdentifier(child.initializer.expression) && child.initializer.expression.text === "useCallback")) {
|
|
117
|
+
handlerNames.add(child.name.text);
|
|
118
|
+
} else if (ts.isFunctionDeclaration(child) && child.name && isHandlerName(child.name.text)) {
|
|
119
|
+
handlerNames.add(child.name.text);
|
|
120
|
+
}
|
|
121
|
+
ts.forEachChild(child, collectHandlers);
|
|
122
|
+
};
|
|
123
|
+
ts.forEachChild(node, collectHandlers);
|
|
124
|
+
if (handlerNames.size === 0) return false;
|
|
125
|
+
let found = false;
|
|
126
|
+
const findPassedHandler = (child) => {
|
|
127
|
+
if (found) return;
|
|
128
|
+
let attributes;
|
|
129
|
+
let tagName2;
|
|
130
|
+
if (ts.isJsxElement(child)) {
|
|
131
|
+
attributes = child.openingElement.attributes;
|
|
132
|
+
tagName2 = child.openingElement.tagName;
|
|
133
|
+
} else if (ts.isJsxSelfClosingElement(child)) {
|
|
134
|
+
attributes = child.attributes;
|
|
135
|
+
tagName2 = child.tagName;
|
|
136
|
+
}
|
|
137
|
+
if (attributes && tagName2 && isComponentTagName(tagName2)) {
|
|
138
|
+
for (const property of attributes.properties) {
|
|
139
|
+
if (!ts.isJsxAttribute(property) || !ts.isIdentifier(property.name) || !/^on[A-Z]/.test(property.name.text)) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const initializer = property.initializer;
|
|
143
|
+
if (!initializer || !ts.isJsxExpression(initializer) || !initializer.expression) continue;
|
|
144
|
+
if (ts.isIdentifier(initializer.expression) && handlerNames.has(initializer.expression.text)) {
|
|
145
|
+
found = true;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
ts.forEachChild(child, findPassedHandler);
|
|
151
|
+
};
|
|
152
|
+
ts.forEachChild(node, findPassedHandler);
|
|
153
|
+
return found;
|
|
154
|
+
}
|
|
155
|
+
function isSmartComponent(node) {
|
|
156
|
+
return containsDataFetch(node) || containsOwnedHandlerProp(node);
|
|
114
157
|
}
|
|
115
158
|
function componentFromFunction(node) {
|
|
116
159
|
const name = node.name?.text;
|
|
117
160
|
if (!name || !isPascalCase(name) || !containsJsx(node)) return void 0;
|
|
118
|
-
return { name, declaration: node, smart:
|
|
161
|
+
return { name, declaration: node, smart: isSmartComponent(node) };
|
|
119
162
|
}
|
|
120
163
|
function componentFromVariable(node) {
|
|
121
164
|
if (!ts.isIdentifier(node.name) || !isPascalCase(node.name.text)) return void 0;
|
|
@@ -125,7 +168,7 @@ function componentFromVariable(node) {
|
|
|
125
168
|
return {
|
|
126
169
|
name: node.name.text,
|
|
127
170
|
declaration: node,
|
|
128
|
-
smart:
|
|
171
|
+
smart: isSmartComponent(initializer)
|
|
129
172
|
};
|
|
130
173
|
}
|
|
131
174
|
function parseComponentInfo(text, filename, options) {
|
|
@@ -221,6 +264,86 @@ function findSimpleRoot(component, _text, _filename) {
|
|
|
221
264
|
if (roots.some((root) => root.tagName !== firstTag)) return void 0;
|
|
222
265
|
return roots[0];
|
|
223
266
|
}
|
|
267
|
+
function literalTestIds(attributes) {
|
|
268
|
+
return attributes.flatMap((candidate) => {
|
|
269
|
+
if (!ts.isJsxAttribute(candidate) || !ts.isIdentifier(candidate.name) || candidate.name.text !== "data-testid") {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
return candidate.initializer && ts.isStringLiteral(candidate.initializer) ? [candidate.initializer.text] : [];
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
function combineTestIdPaths(left, right) {
|
|
276
|
+
return left.flatMap((leftPath) => right.map((rightPath) => [...leftPath, ...rightPath]));
|
|
277
|
+
}
|
|
278
|
+
function renderedTestIdPathsFromExpression(expression) {
|
|
279
|
+
let unwrapped = expression;
|
|
280
|
+
while (ts.isParenthesizedExpression(unwrapped)) unwrapped = unwrapped.expression;
|
|
281
|
+
if (ts.isConditionalExpression(unwrapped)) {
|
|
282
|
+
return [
|
|
283
|
+
...renderedTestIdPathsFromExpression(unwrapped.whenTrue),
|
|
284
|
+
...renderedTestIdPathsFromExpression(unwrapped.whenFalse)
|
|
285
|
+
];
|
|
286
|
+
}
|
|
287
|
+
if (ts.isJsxSelfClosingElement(unwrapped)) {
|
|
288
|
+
return [literalTestIds(unwrapped.attributes.properties)];
|
|
289
|
+
}
|
|
290
|
+
if (ts.isJsxElement(unwrapped)) {
|
|
291
|
+
let paths = [literalTestIds(unwrapped.openingElement.attributes.properties)];
|
|
292
|
+
for (const child of unwrapped.children) {
|
|
293
|
+
if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child) || ts.isJsxFragment(child)) {
|
|
294
|
+
paths = combineTestIdPaths(paths, renderedTestIdPathsFromExpression(child));
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (ts.isJsxExpression(child) && child.expression) {
|
|
298
|
+
const childExpression = child.expression;
|
|
299
|
+
if (ts.isConditionalExpression(childExpression) || ts.isJsxElement(childExpression) || ts.isJsxSelfClosingElement(childExpression) || ts.isJsxFragment(childExpression) || ts.isParenthesizedExpression(childExpression)) {
|
|
300
|
+
paths = combineTestIdPaths(paths, renderedTestIdPathsFromExpression(childExpression));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return paths;
|
|
305
|
+
}
|
|
306
|
+
if (ts.isJsxFragment(unwrapped)) {
|
|
307
|
+
let paths = [[]];
|
|
308
|
+
for (const child of unwrapped.children) {
|
|
309
|
+
if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child) || ts.isJsxFragment(child)) {
|
|
310
|
+
paths = combineTestIdPaths(paths, renderedTestIdPathsFromExpression(child));
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (ts.isJsxExpression(child) && child.expression) {
|
|
314
|
+
const childExpression = child.expression;
|
|
315
|
+
if (ts.isConditionalExpression(childExpression) || ts.isJsxElement(childExpression) || ts.isJsxSelfClosingElement(childExpression) || ts.isJsxFragment(childExpression) || ts.isParenthesizedExpression(childExpression)) {
|
|
316
|
+
paths = combineTestIdPaths(paths, renderedTestIdPathsFromExpression(childExpression));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return paths;
|
|
321
|
+
}
|
|
322
|
+
return [[]];
|
|
323
|
+
}
|
|
324
|
+
function findRenderedTestIdPaths(component) {
|
|
325
|
+
const declaration = component.declaration;
|
|
326
|
+
let body;
|
|
327
|
+
if (ts.isFunctionDeclaration(declaration)) body = declaration.body;
|
|
328
|
+
else if (declaration.initializer && ts.isArrowFunction(declaration.initializer)) body = declaration.initializer.body;
|
|
329
|
+
else if (declaration.initializer && ts.isFunctionExpression(declaration.initializer)) {
|
|
330
|
+
body = declaration.initializer.body;
|
|
331
|
+
}
|
|
332
|
+
if (!body) return [];
|
|
333
|
+
if (!ts.isBlock(body)) return renderedTestIdPathsFromExpression(body);
|
|
334
|
+
const returns = [];
|
|
335
|
+
const visit = (node) => {
|
|
336
|
+
if (node !== body && (ts.isFunctionLike(node) || ts.isClassLike(node))) return;
|
|
337
|
+
if (ts.isReturnStatement(node)) returns.push(node);
|
|
338
|
+
ts.forEachChild(node, visit);
|
|
339
|
+
};
|
|
340
|
+
visit(body);
|
|
341
|
+
return returns.flatMap((statement) => {
|
|
342
|
+
const expression = statement.expression;
|
|
343
|
+
if (!expression || expression.kind === ts.SyntaxKind.NullKeyword) return [];
|
|
344
|
+
return renderedTestIdPathsFromExpression(expression);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
224
347
|
function getTestId(root) {
|
|
225
348
|
const attribute = root.attributes.find(
|
|
226
349
|
(candidate) => ts.isJsxAttribute(candidate) && ts.isIdentifier(candidate.name) && candidate.name.text === "data-testid"
|
|
@@ -2150,7 +2273,6 @@ var NEXT_ROUTING_FILES2 = /* @__PURE__ */ new Set([
|
|
|
2150
2273
|
"sitemap",
|
|
2151
2274
|
"twitter-image"
|
|
2152
2275
|
]);
|
|
2153
|
-
var isPascalCase5 = (value) => /^[A-Z][A-Za-z0-9]*$/.test(value);
|
|
2154
2276
|
var isKebabCase3 = (value) => /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value);
|
|
2155
2277
|
var dataTestIdCaseRule = {
|
|
2156
2278
|
meta: {
|
|
@@ -2170,28 +2292,34 @@ var dataTestIdCaseRule = {
|
|
|
2170
2292
|
return {
|
|
2171
2293
|
Program(node) {
|
|
2172
2294
|
for (const component of components) {
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
const
|
|
2176
|
-
const
|
|
2177
|
-
if (
|
|
2295
|
+
if (component.smart) {
|
|
2296
|
+
const expected2 = component.name;
|
|
2297
|
+
const renderedPaths = findRenderedTestIdPaths(component);
|
|
2298
|
+
const hasStableAnchor = renderedPaths.length > 0 && renderedPaths.every((values) => values.filter((value2) => value2 === expected2).length === 1);
|
|
2299
|
+
if (hasStableAnchor) continue;
|
|
2300
|
+
const hasSingleWrongCasedAnchor = renderedPaths.length > 0 && renderedPaths.every((values) => values.length === 1 && values[0] !== expected2) && new Set(renderedPaths.map((values) => values[0])).size === 1;
|
|
2301
|
+
if (hasSingleWrongCasedAnchor) {
|
|
2178
2302
|
context.report({
|
|
2179
2303
|
node,
|
|
2180
|
-
message: `
|
|
2304
|
+
message: `data-testid for smart component "${component.name}" must be PascalCase: expected "${expected2}".`
|
|
2181
2305
|
});
|
|
2182
2306
|
continue;
|
|
2183
2307
|
}
|
|
2184
|
-
if (value === void 0 || value === expected) continue;
|
|
2185
|
-
if (component.smart ? !isPascalCase5(value) || value !== expected : !isKebabCase3(value)) {
|
|
2186
|
-
context.report({
|
|
2187
|
-
node,
|
|
2188
|
-
message: `data-testid for ${component.smart ? "smart" : "dumb"} component "${component.name}" must be ${component.smart ? "PascalCase" : "kebab-case"}: expected "${expected}".`
|
|
2189
|
-
});
|
|
2190
|
-
}
|
|
2191
|
-
} else if (component.smart) {
|
|
2192
2308
|
context.report({
|
|
2193
2309
|
node,
|
|
2194
|
-
message: `Smart component "${component.name}"
|
|
2310
|
+
message: `Smart component "${component.name}" must expose exactly one stable data-testid="${expected2}" anchor for itself in every rendered result. Place it on the existing DOM surface or on a child component that forwards data-testid instead of adding an artificial wrapper. See docs/next-codebase-guide/rules/smart-vs-dumb-component-rule.md`
|
|
2311
|
+
});
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
const root = findSimpleRoot(component, text, filename);
|
|
2315
|
+
if (!root) continue;
|
|
2316
|
+
const { value } = getTestId(root);
|
|
2317
|
+
const expected = toKebabCase(component.name);
|
|
2318
|
+
if (value === void 0 || value === expected) continue;
|
|
2319
|
+
if (!isKebabCase3(value)) {
|
|
2320
|
+
context.report({
|
|
2321
|
+
node,
|
|
2322
|
+
message: `data-testid for dumb component "${component.name}" must be kebab-case: expected "${expected}".`
|
|
2195
2323
|
});
|
|
2196
2324
|
}
|
|
2197
2325
|
}
|
|
@@ -4794,7 +4922,7 @@ var schemaCasingRule = {
|
|
|
4794
4922
|
|
|
4795
4923
|
// eslint/rules/component-casing.ts
|
|
4796
4924
|
import path42 from "path";
|
|
4797
|
-
function
|
|
4925
|
+
function isPascalCase5(name) {
|
|
4798
4926
|
return /^[A-Z][A-Za-z0-9]*$/.test(name);
|
|
4799
4927
|
}
|
|
4800
4928
|
var componentCasingRule = {
|
|
@@ -4813,7 +4941,7 @@ var componentCasingRule = {
|
|
|
4813
4941
|
return {
|
|
4814
4942
|
Program(node) {
|
|
4815
4943
|
for (const declaration of findJsxReturningDeclarations(context.sourceCode.text, filename)) {
|
|
4816
|
-
if (
|
|
4944
|
+
if (isPascalCase5(declaration.name)) continue;
|
|
4817
4945
|
context.report({
|
|
4818
4946
|
node,
|
|
4819
4947
|
loc: { line: declaration.line, column: declaration.column },
|