pasika 0.3.4 → 0.3.5
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/README.md +3 -0
- package/dist/cli/index.js +507 -85
- package/dist/eslint/pasika/index.d.ts +4 -0
- package/dist/eslint/pasika/index.js +481 -59
- package/package.json +1 -1
|
@@ -586,8 +586,8 @@ var enforceCvaVariantPropsRule = {
|
|
|
586
586
|
if (node.typeAnnotation?.type !== "TSTypeLiteral") return;
|
|
587
587
|
for (const member of node.typeAnnotation.members ?? []) {
|
|
588
588
|
if (member.type !== "TSPropertySignature") continue;
|
|
589
|
-
const
|
|
590
|
-
if (!
|
|
589
|
+
const keyName2 = member.key?.type === "Identifier" ? member.key.name : void 0;
|
|
590
|
+
if (!keyName2) continue;
|
|
591
591
|
const typeAnn = member.typeAnnotation?.typeAnnotation;
|
|
592
592
|
if (typeAnn?.type !== "TSUnionType") continue;
|
|
593
593
|
const allStringLiterals = (typeAnn.types ?? []).every(
|
|
@@ -595,10 +595,10 @@ var enforceCvaVariantPropsRule = {
|
|
|
595
595
|
);
|
|
596
596
|
if (allStringLiterals && (typeAnn.types?.length ?? 0) >= 2) {
|
|
597
597
|
for (const variantNames of cvaDefinitions.values()) {
|
|
598
|
-
if (variantNames.includes(
|
|
598
|
+
if (variantNames.includes(keyName2)) {
|
|
599
599
|
context.report({
|
|
600
600
|
node,
|
|
601
|
-
message: `Variant prop "${
|
|
601
|
+
message: `Variant prop "${keyName2}" duplicates CVA variant values. Use VariantProps<typeof variants> instead of manually writing union types. See docs/styling-guide/rules/component-variant-rule.md`
|
|
602
602
|
});
|
|
603
603
|
return;
|
|
604
604
|
}
|
|
@@ -1334,21 +1334,27 @@ var dataTestIdCaseRule = {
|
|
|
1334
1334
|
Program(node) {
|
|
1335
1335
|
for (const component of components) {
|
|
1336
1336
|
const root = findSimpleRoot(component, text, filename);
|
|
1337
|
-
if (
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1337
|
+
if (root) {
|
|
1338
|
+
const { value } = getTestId(root);
|
|
1339
|
+
const expected = component.smart ? component.name : toKebabCase(component.name);
|
|
1340
|
+
if (component.smart && value === void 0) {
|
|
1341
|
+
context.report({
|
|
1342
|
+
node,
|
|
1343
|
+
message: `Smart component "${component.name}" with one outer DOM element must set data-testid="${expected}".`
|
|
1344
|
+
});
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
if (value === void 0 || value === expected) continue;
|
|
1348
|
+
if (component.smart ? !isPascalCase5(value) || value !== expected : !isKebabCase3(value)) {
|
|
1349
|
+
context.report({
|
|
1350
|
+
node,
|
|
1351
|
+
message: `data-testid for ${component.smart ? "smart" : "dumb"} component "${component.name}" must be ${component.smart ? "PascalCase" : "kebab-case"}: expected "${expected}".`
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
} else if (component.smart) {
|
|
1349
1355
|
context.report({
|
|
1350
1356
|
node,
|
|
1351
|
-
message: `
|
|
1357
|
+
message: `Smart component "${component.name}" has no single outer element; wrap its content in one outer element with data-testid="${component.name}" instead of rendering multiple roots. See docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md`
|
|
1352
1358
|
});
|
|
1353
1359
|
}
|
|
1354
1360
|
}
|
|
@@ -1831,14 +1837,14 @@ var cvaAppearancePropsRule = {
|
|
|
1831
1837
|
if (node.typeAnnotation?.type !== "TSTypeLiteral") return;
|
|
1832
1838
|
for (const member of node.typeAnnotation.members ?? []) {
|
|
1833
1839
|
if (member.type !== "TSPropertySignature") continue;
|
|
1834
|
-
const
|
|
1835
|
-
if (!
|
|
1840
|
+
const keyName2 = member.key?.type === "Identifier" ? member.key.name : void 0;
|
|
1841
|
+
if (!keyName2 || !VISUAL_OPTION_NAMES.has(keyName2)) continue;
|
|
1836
1842
|
const typeAnnotation = member.typeAnnotation?.typeAnnotation ?? member.typeAnnotation;
|
|
1837
1843
|
if (!typeAnnotation) continue;
|
|
1838
1844
|
if (typeAnnotation.type === "TSUnionType" || typeAnnotation.type === "TSTypeLiteral") {
|
|
1839
1845
|
context.report({
|
|
1840
1846
|
node,
|
|
1841
|
-
message: `Visual option prop "${
|
|
1847
|
+
message: `Visual option prop "${keyName2}" must be defined through a cva() call rather than a manual union type. See docs/styling-guide/rules/component-variant-rule.md`
|
|
1842
1848
|
});
|
|
1843
1849
|
}
|
|
1844
1850
|
}
|
|
@@ -1858,15 +1864,15 @@ function extractCvaInfo(node) {
|
|
|
1858
1864
|
if (options?.type !== "ObjectExpression") return { variantNames, compoundVariants };
|
|
1859
1865
|
for (const prop of options.properties) {
|
|
1860
1866
|
if (prop.type !== "Property") continue;
|
|
1861
|
-
const
|
|
1862
|
-
if (
|
|
1867
|
+
const keyName2 = prop.key.type === "Identifier" ? prop.key.name : void 0;
|
|
1868
|
+
if (keyName2 === "variants" && prop.value.type === "ObjectExpression") {
|
|
1863
1869
|
for (const vProp of prop.value.properties) {
|
|
1864
1870
|
if (vProp.type !== "Property") continue;
|
|
1865
1871
|
const vName = vProp.key.type === "Identifier" ? vProp.key.name : void 0;
|
|
1866
1872
|
if (vName) variantNames.add(vName);
|
|
1867
1873
|
}
|
|
1868
1874
|
}
|
|
1869
|
-
if (
|
|
1875
|
+
if (keyName2 === "compoundVariants" && prop.value.type === "ArrayExpression") {
|
|
1870
1876
|
for (const entry of prop.value.elements) {
|
|
1871
1877
|
if (entry?.type !== "ObjectExpression") continue;
|
|
1872
1878
|
for (const cvProp of entry.properties) {
|
|
@@ -2637,8 +2643,279 @@ var localePlacementRule = {
|
|
|
2637
2643
|
}
|
|
2638
2644
|
};
|
|
2639
2645
|
|
|
2640
|
-
// eslint/pasika/rules/
|
|
2646
|
+
// eslint/pasika/rules/sole-state-owner.ts
|
|
2641
2647
|
import path29 from "path";
|
|
2648
|
+
import ts6 from "typescript";
|
|
2649
|
+
function findStateHooks(node) {
|
|
2650
|
+
const hooks = [];
|
|
2651
|
+
const visit = (child) => {
|
|
2652
|
+
if (ts6.isCallExpression(child) && ts6.isIdentifier(child.expression) && child.expression.text === "useState") {
|
|
2653
|
+
if (ts6.isVariableDeclaration(child.parent)) {
|
|
2654
|
+
const { name } = child.parent;
|
|
2655
|
+
if (ts6.isArrayBindingPattern(name) && name.elements.length >= 2) {
|
|
2656
|
+
const value = name.elements[0];
|
|
2657
|
+
const updater = name.elements[1];
|
|
2658
|
+
if (value && updater && ts6.isBindingElement(value) && ts6.isBindingElement(updater)) {
|
|
2659
|
+
const valueName = value.name;
|
|
2660
|
+
const updaterName = updater.name;
|
|
2661
|
+
if (ts6.isIdentifier(valueName) && ts6.isIdentifier(updaterName)) {
|
|
2662
|
+
hooks.push({ value: valueName.text, updater: updaterName.text });
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
ts6.forEachChild(child, visit);
|
|
2669
|
+
};
|
|
2670
|
+
visit(node);
|
|
2671
|
+
return hooks;
|
|
2672
|
+
}
|
|
2673
|
+
function isHookUsage(node, hook) {
|
|
2674
|
+
if (ts6.isCallExpression(node) && ts6.isIdentifier(node.expression) && node.expression.text === hook.updater) {
|
|
2675
|
+
return "updater";
|
|
2676
|
+
}
|
|
2677
|
+
if (ts6.isIdentifier(node) && node.text === hook.value) {
|
|
2678
|
+
const parent = node.parent;
|
|
2679
|
+
if (ts6.isBindingElement(parent) || ts6.isPropertyAccessExpression(parent) || ts6.isShorthandPropertyAssignment(parent)) {
|
|
2680
|
+
return void 0;
|
|
2681
|
+
}
|
|
2682
|
+
return "value";
|
|
2683
|
+
}
|
|
2684
|
+
return void 0;
|
|
2685
|
+
}
|
|
2686
|
+
function topLevelJsxChildren(initial) {
|
|
2687
|
+
if (!initial) return void 0;
|
|
2688
|
+
let expression = initial;
|
|
2689
|
+
while (ts6.isParenthesizedExpression(expression)) expression = expression.expression;
|
|
2690
|
+
if (ts6.isJsxFragment(expression)) {
|
|
2691
|
+
const children = expression.children.filter(
|
|
2692
|
+
(c) => !ts6.isJsxText(c) && !ts6.isJsxSpreadAttribute(c)
|
|
2693
|
+
);
|
|
2694
|
+
return { root: expression, children };
|
|
2695
|
+
}
|
|
2696
|
+
if (ts6.isJsxElement(expression)) {
|
|
2697
|
+
const children = expression.children.filter(
|
|
2698
|
+
(c) => !ts6.isJsxText(c) && !ts6.isJsxSpreadAttribute(c)
|
|
2699
|
+
);
|
|
2700
|
+
return { root: expression, children };
|
|
2701
|
+
}
|
|
2702
|
+
return void 0;
|
|
2703
|
+
}
|
|
2704
|
+
function collectUsesIn(child, hook) {
|
|
2705
|
+
const positions = [];
|
|
2706
|
+
let count = 0;
|
|
2707
|
+
const visit = (node) => {
|
|
2708
|
+
if (isHookUsage(node, hook)) {
|
|
2709
|
+
positions.push(node);
|
|
2710
|
+
count += 1;
|
|
2711
|
+
}
|
|
2712
|
+
if (ts6.isFunctionDeclaration(node) || ts6.isClassDeclaration(node)) return;
|
|
2713
|
+
ts6.forEachChild(node, visit);
|
|
2714
|
+
};
|
|
2715
|
+
visit(child);
|
|
2716
|
+
return { positions, count };
|
|
2717
|
+
}
|
|
2718
|
+
var soleStateOwnerRule = {
|
|
2719
|
+
meta: {
|
|
2720
|
+
schema: [],
|
|
2721
|
+
type: "problem",
|
|
2722
|
+
docs: {
|
|
2723
|
+
description: "Require extracting a JSX part that is the sole owner of a useState hook as a named component."
|
|
2724
|
+
}
|
|
2725
|
+
},
|
|
2726
|
+
create(context) {
|
|
2727
|
+
const filename = path29.resolve(context.filename);
|
|
2728
|
+
if (!filename.endsWith(".tsx") && !filename.endsWith(".jsx")) return {};
|
|
2729
|
+
const text = context.sourceCode.text;
|
|
2730
|
+
const components = parseComponentInfo(text, filename);
|
|
2731
|
+
return {
|
|
2732
|
+
Program(node) {
|
|
2733
|
+
for (const component of components) {
|
|
2734
|
+
const hooks = findStateHooks(component.declaration);
|
|
2735
|
+
for (const hook of hooks) {
|
|
2736
|
+
const result = analyzeSoleOwner(component.declaration, hook);
|
|
2737
|
+
if (!result) continue;
|
|
2738
|
+
const { run, totalTopLevel } = result;
|
|
2739
|
+
if (run === 0 || totalTopLevel === 0) continue;
|
|
2740
|
+
context.report({
|
|
2741
|
+
node,
|
|
2742
|
+
loc: { line: 1, column: 0 },
|
|
2743
|
+
message: `Component "${component.name}" uses state "${hook.value}" in ${String(run)} contiguous top-level JSX part${run === 1 ? "" : "s"}; extract that part into a named component that owns useState instead of reading it in the parent. See docs/code-organization-guide/rules/sole-state-owner-rule.md`
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
};
|
|
2751
|
+
function analyzeSoleOwner(declaration, hook) {
|
|
2752
|
+
const body = ts6.isFunctionDeclaration(declaration) ? declaration.body : void 0;
|
|
2753
|
+
if (!body || !ts6.isBlock(body)) return void 0;
|
|
2754
|
+
const returns = [];
|
|
2755
|
+
const visit = (node) => {
|
|
2756
|
+
if (node !== body && (ts6.isFunctionLike(node) || ts6.isClassLike(node))) return;
|
|
2757
|
+
if (ts6.isReturnStatement(node)) returns.push(node);
|
|
2758
|
+
ts6.forEachChild(node, visit);
|
|
2759
|
+
};
|
|
2760
|
+
visit(body);
|
|
2761
|
+
if (returns.length !== 1) return void 0;
|
|
2762
|
+
const single = returns[0];
|
|
2763
|
+
if (!single?.expression || !ts6.isExpression(single.expression)) return void 0;
|
|
2764
|
+
const returnExpression = single.expression;
|
|
2765
|
+
const root = topLevelJsxChildren(returnExpression);
|
|
2766
|
+
if (!root || root.children.length === 0) return void 0;
|
|
2767
|
+
const totalTopLevel = root.children.length;
|
|
2768
|
+
const usagePerChild = root.children.map((child) => collectUsesIn(child, hook));
|
|
2769
|
+
const outsideJsx = usesOutsideJsx(declaration, hook, root.children);
|
|
2770
|
+
if (outsideJsx) return void 0;
|
|
2771
|
+
const usedLabels = usagePerChild.map((u) => u.count > 0);
|
|
2772
|
+
const firstUsed = usedLabels.findIndex((v) => v);
|
|
2773
|
+
const lastUsed = usedLabels.lastIndexOf(true);
|
|
2774
|
+
if (firstUsed === -1 || lastUsed === -1) return void 0;
|
|
2775
|
+
const windowHasUnused = usedLabels.slice(firstUsed, lastUsed + 1).some((v) => !v);
|
|
2776
|
+
if (windowHasUnused) return void 0;
|
|
2777
|
+
const run = lastUsed - firstUsed + 1;
|
|
2778
|
+
const hasOutside = firstUsed > 0 || lastUsed < totalTopLevel - 1;
|
|
2779
|
+
if (!hasOutside) return void 0;
|
|
2780
|
+
return { run, totalTopLevel };
|
|
2781
|
+
}
|
|
2782
|
+
function usesOutsideJsx(declaration, hook, children) {
|
|
2783
|
+
let outside = false;
|
|
2784
|
+
const visit = (node) => {
|
|
2785
|
+
if (outside) return;
|
|
2786
|
+
if (node !== declaration && (ts6.isFunctionDeclaration(node) || ts6.isClassDeclaration(node))) return;
|
|
2787
|
+
if (isHookUsage(node, hook)) {
|
|
2788
|
+
let current = node;
|
|
2789
|
+
let isInJsxChild = false;
|
|
2790
|
+
while (!ts6.isSourceFile(current)) {
|
|
2791
|
+
if (children.includes(current)) {
|
|
2792
|
+
isInJsxChild = true;
|
|
2793
|
+
break;
|
|
2794
|
+
}
|
|
2795
|
+
current = current.parent;
|
|
2796
|
+
}
|
|
2797
|
+
if (!isInJsxChild) outside = true;
|
|
2798
|
+
}
|
|
2799
|
+
ts6.forEachChild(node, visit);
|
|
2800
|
+
};
|
|
2801
|
+
visit(declaration);
|
|
2802
|
+
return outside;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// eslint/pasika/rules/locale-key-shape.ts
|
|
2806
|
+
import path30 from "path";
|
|
2807
|
+
var MAX_KEY_LENGTH = 30;
|
|
2808
|
+
var ROLE_POSTFIXES = /* @__PURE__ */ new Set([
|
|
2809
|
+
"Button",
|
|
2810
|
+
"Link",
|
|
2811
|
+
"Dialog",
|
|
2812
|
+
"AlertDialog",
|
|
2813
|
+
"Tooltip",
|
|
2814
|
+
"Banner",
|
|
2815
|
+
"Alert",
|
|
2816
|
+
"Status",
|
|
2817
|
+
"Tab",
|
|
2818
|
+
"TabPanel",
|
|
2819
|
+
"Menu",
|
|
2820
|
+
"MenuItem",
|
|
2821
|
+
"MenuBar",
|
|
2822
|
+
"Checkbox",
|
|
2823
|
+
"Radio",
|
|
2824
|
+
"RadioGroup",
|
|
2825
|
+
"Switch",
|
|
2826
|
+
"ComboBox",
|
|
2827
|
+
"ListBox",
|
|
2828
|
+
"Option",
|
|
2829
|
+
"Search",
|
|
2830
|
+
"SearchBox",
|
|
2831
|
+
"Navigation",
|
|
2832
|
+
"ProgressBar",
|
|
2833
|
+
"Slider",
|
|
2834
|
+
"SpinButton",
|
|
2835
|
+
"TextBox",
|
|
2836
|
+
"Tree",
|
|
2837
|
+
"TreeItem",
|
|
2838
|
+
"TreeGrid",
|
|
2839
|
+
"Grid",
|
|
2840
|
+
"GridCell",
|
|
2841
|
+
"Feed",
|
|
2842
|
+
"Log",
|
|
2843
|
+
"Timer",
|
|
2844
|
+
"Marquee",
|
|
2845
|
+
"Region",
|
|
2846
|
+
"Main",
|
|
2847
|
+
"Complementary",
|
|
2848
|
+
"ContentInfo",
|
|
2849
|
+
"Form",
|
|
2850
|
+
"Toolbar",
|
|
2851
|
+
"ScrollBar"
|
|
2852
|
+
]);
|
|
2853
|
+
var CAMEL_CASE = /^[a-z][a-zA-Z0-9]*$/;
|
|
2854
|
+
function isLocalesFile2(filename) {
|
|
2855
|
+
const segments = path30.resolve(filename).split(path30.sep);
|
|
2856
|
+
const srcIdx = segments.lastIndexOf("src");
|
|
2857
|
+
return srcIdx !== -1 && segments[srcIdx + 1] === "locales";
|
|
2858
|
+
}
|
|
2859
|
+
function keyName(key) {
|
|
2860
|
+
if (key.type === "Identifier") return key.name;
|
|
2861
|
+
if (key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
2862
|
+
return void 0;
|
|
2863
|
+
}
|
|
2864
|
+
function reportFor(context, node, message) {
|
|
2865
|
+
context.report({ node, message });
|
|
2866
|
+
}
|
|
2867
|
+
function checkKey(context, node, name) {
|
|
2868
|
+
if (!CAMEL_CASE.test(name)) {
|
|
2869
|
+
reportFor(
|
|
2870
|
+
context,
|
|
2871
|
+
node,
|
|
2872
|
+
`Locale key "${name}" must be camelCase English based on the text. See docs/code-organization-guide/rules/locales-rule.md`
|
|
2873
|
+
);
|
|
2874
|
+
return;
|
|
2875
|
+
}
|
|
2876
|
+
if (name.length <= MAX_KEY_LENGTH) return;
|
|
2877
|
+
const hasRolePostfix = [...ROLE_POSTFIXES].some((role) => name.endsWith(role));
|
|
2878
|
+
if (hasRolePostfix) return;
|
|
2879
|
+
reportFor(
|
|
2880
|
+
context,
|
|
2881
|
+
node,
|
|
2882
|
+
`Locale key "${name}" is longer than ${String(MAX_KEY_LENGTH)} characters, so it must describe the message's purpose instead of the text; end it with an element role such as "Button", "Link", or "Dialog" (see WAI-ARIA roles). See docs/code-organization-guide/rules/locales-rule.md`
|
|
2883
|
+
);
|
|
2884
|
+
}
|
|
2885
|
+
var localeKeyShapeRule = {
|
|
2886
|
+
meta: {
|
|
2887
|
+
schema: [],
|
|
2888
|
+
type: "problem",
|
|
2889
|
+
docs: {
|
|
2890
|
+
description: "Require locale keys to be camelCase, and purpose-based element roles when long."
|
|
2891
|
+
}
|
|
2892
|
+
},
|
|
2893
|
+
create(context) {
|
|
2894
|
+
if (!isLocalesFile2(context.filename)) return {};
|
|
2895
|
+
function visitObject(node) {
|
|
2896
|
+
for (const property of node.properties) {
|
|
2897
|
+
if (property.type !== "Property") continue;
|
|
2898
|
+
const name = keyName(property.key);
|
|
2899
|
+
if (name === void 0) continue;
|
|
2900
|
+
if (property.value.type === "ObjectExpression") {
|
|
2901
|
+
visitObject(property.value);
|
|
2902
|
+
} else if (property.value.type === "Literal" && typeof property.value.value === "string") {
|
|
2903
|
+
checkKey(context, property, name);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
return {
|
|
2908
|
+
VariableDeclarator(node) {
|
|
2909
|
+
if (node.id.type !== "Identifier" || node.id.name !== "locales") return;
|
|
2910
|
+
if (node.init?.type !== "ObjectExpression") return;
|
|
2911
|
+
visitObject(node.init);
|
|
2912
|
+
}
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
};
|
|
2916
|
+
|
|
2917
|
+
// eslint/pasika/rules/shared-style-dedup.ts
|
|
2918
|
+
import path31 from "path";
|
|
2642
2919
|
import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
2643
2920
|
var CLASS_NAME = /className="(?<classes>[^"]+)"/g;
|
|
2644
2921
|
var comboCache;
|
|
@@ -2679,8 +2956,8 @@ var sharedStyleDedupRule = {
|
|
|
2679
2956
|
}
|
|
2680
2957
|
},
|
|
2681
2958
|
create(context) {
|
|
2682
|
-
const sourceRoot2 =
|
|
2683
|
-
const file =
|
|
2959
|
+
const sourceRoot2 = path31.resolve("src");
|
|
2960
|
+
const file = path31.resolve(context.filename);
|
|
2684
2961
|
const segments = segmentsOf(file, sourceRoot2);
|
|
2685
2962
|
if (segments.length === 0) return {};
|
|
2686
2963
|
const index = getProjectIndex(sourceRoot2);
|
|
@@ -2702,6 +2979,114 @@ var sharedStyleDedupRule = {
|
|
|
2702
2979
|
}
|
|
2703
2980
|
};
|
|
2704
2981
|
|
|
2982
|
+
// eslint/pasika/rules/repeated-structure.ts
|
|
2983
|
+
function nodeKind(node) {
|
|
2984
|
+
return node.type;
|
|
2985
|
+
}
|
|
2986
|
+
function isJsxNode(node) {
|
|
2987
|
+
const kind = nodeKind(node);
|
|
2988
|
+
return kind === "JSXElement" || kind === "JSXFragment";
|
|
2989
|
+
}
|
|
2990
|
+
function isJsxExpressionContainer(node) {
|
|
2991
|
+
return node.type === "JSXExpressionContainer";
|
|
2992
|
+
}
|
|
2993
|
+
function walkJsx(current, visit) {
|
|
2994
|
+
if (isJsxNode(current)) {
|
|
2995
|
+
visit(current);
|
|
2996
|
+
for (const child of current.children ?? []) walkJsx(child, visit);
|
|
2997
|
+
} else if (isJsxExpressionContainer(current)) {
|
|
2998
|
+
if (current.expression) walkJsx(current.expression, visit);
|
|
2999
|
+
} else if (current.type === "ArrowFunctionExpression") {
|
|
3000
|
+
walkJsx(current.body, visit);
|
|
3001
|
+
} else if (current.type === "CallExpression") {
|
|
3002
|
+
for (const argument of current.arguments) walkJsx(argument, visit);
|
|
3003
|
+
} else if (current.type === "ConditionalExpression") {
|
|
3004
|
+
walkJsx(current.consequent, visit);
|
|
3005
|
+
walkJsx(current.alternate, visit);
|
|
3006
|
+
} else if (current.type === "LogicalExpression") {
|
|
3007
|
+
walkJsx(current.left, visit);
|
|
3008
|
+
walkJsx(current.right, visit);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
function signature(node) {
|
|
3012
|
+
const parts = [];
|
|
3013
|
+
const visit = (current) => {
|
|
3014
|
+
const opening = nodeKind(current) === "JSXFragment" ? "<>" : current.openingElement?.name.name ?? "<>";
|
|
3015
|
+
parts.push(opening);
|
|
3016
|
+
if (nodeKind(current) === "JSXElement") {
|
|
3017
|
+
const attributes = (current.openingElement?.attributes ?? []).filter((attribute) => attribute.type === "JSXAttribute").map((attribute) => {
|
|
3018
|
+
const name = attribute.name?.name ?? "";
|
|
3019
|
+
if (name === "className" && attribute.value?.type === "Literal" && typeof attribute.value.value === "string") {
|
|
3020
|
+
return `class="${attribute.value.value.split(/\s+/).filter(Boolean).sort().join(" ")}"`;
|
|
3021
|
+
}
|
|
3022
|
+
return name;
|
|
3023
|
+
}).sort();
|
|
3024
|
+
parts.push(`[${attributes.join(",")}]`);
|
|
3025
|
+
}
|
|
3026
|
+
for (const child of current.children ?? []) {
|
|
3027
|
+
if (isJsxNode(child)) {
|
|
3028
|
+
visit(child);
|
|
3029
|
+
} else if (isJsxExpressionContainer(child)) {
|
|
3030
|
+
walkJsx(child, visit);
|
|
3031
|
+
parts.push("{}");
|
|
3032
|
+
} else {
|
|
3033
|
+
parts.push("{}");
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
};
|
|
3037
|
+
visit(node);
|
|
3038
|
+
return parts.join("/");
|
|
3039
|
+
}
|
|
3040
|
+
function isSubstantial(node) {
|
|
3041
|
+
let count = 0;
|
|
3042
|
+
walkJsx(node, () => {
|
|
3043
|
+
count += 1;
|
|
3044
|
+
});
|
|
3045
|
+
return count >= 3;
|
|
3046
|
+
}
|
|
3047
|
+
function blockChildren(node) {
|
|
3048
|
+
return (node.children ?? []).filter(isJsxNode);
|
|
3049
|
+
}
|
|
3050
|
+
var repeatedStructureRule = {
|
|
3051
|
+
meta: {
|
|
3052
|
+
schema: [],
|
|
3053
|
+
type: "problem",
|
|
3054
|
+
docs: {
|
|
3055
|
+
description: "Require extracting a repeated block of elements as a named component."
|
|
3056
|
+
}
|
|
3057
|
+
},
|
|
3058
|
+
create(context) {
|
|
3059
|
+
if (!context.filename.endsWith(".tsx") && !context.filename.endsWith(".jsx")) return {};
|
|
3060
|
+
return {
|
|
3061
|
+
JSXElement(node) {
|
|
3062
|
+
checkChildren(node);
|
|
3063
|
+
},
|
|
3064
|
+
JSXFragment(node) {
|
|
3065
|
+
checkChildren(node);
|
|
3066
|
+
}
|
|
3067
|
+
};
|
|
3068
|
+
function checkChildren(node) {
|
|
3069
|
+
const children = blockChildren(node);
|
|
3070
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3071
|
+
for (const child of children) {
|
|
3072
|
+
const sig = signature(child);
|
|
3073
|
+
const group = seen.get(sig) ?? [];
|
|
3074
|
+
group.push(child);
|
|
3075
|
+
seen.set(sig, group);
|
|
3076
|
+
}
|
|
3077
|
+
for (const [, group] of seen) {
|
|
3078
|
+
if (group.length < 2) continue;
|
|
3079
|
+
const first = group[0];
|
|
3080
|
+
if (!first || !isSubstantial(first)) continue;
|
|
3081
|
+
context.report({
|
|
3082
|
+
node: first,
|
|
3083
|
+
message: `The same arrangement of elements appears ${String(group.length)} times here; extract it as a named component. Different data or labels do not prevent extraction. See docs/code-organization-guide/rules/repeated-structure-rule.md`
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
};
|
|
3089
|
+
|
|
2705
3090
|
// eslint/pasika/rules/no-eslint-disable.ts
|
|
2706
3091
|
var noEslintDisableRule = {
|
|
2707
3092
|
meta: {
|
|
@@ -2801,7 +3186,7 @@ var zodSchemaValidationRule = {
|
|
|
2801
3186
|
};
|
|
2802
3187
|
|
|
2803
3188
|
// eslint/pasika/rules/source-under-src.ts
|
|
2804
|
-
import
|
|
3189
|
+
import path32 from "path";
|
|
2805
3190
|
var NON_SOURCE_ROOT_DIRS = /* @__PURE__ */ new Set([
|
|
2806
3191
|
".agents",
|
|
2807
3192
|
".cache",
|
|
@@ -2842,14 +3227,14 @@ var sourceUnderSrcRule = {
|
|
|
2842
3227
|
}
|
|
2843
3228
|
},
|
|
2844
3229
|
create(context) {
|
|
2845
|
-
const filename =
|
|
3230
|
+
const filename = path32.resolve(context.filename);
|
|
2846
3231
|
if (!MODULE_EXTENSION.test(filename)) return {};
|
|
2847
|
-
const relative =
|
|
3232
|
+
const relative = path32.relative(process.cwd(), filename).replace(/\\/g, "/");
|
|
2848
3233
|
if (relative === "src" || relative.startsWith("src/")) return {};
|
|
2849
3234
|
const topLevel = relative.split("/")[0] ?? "";
|
|
2850
3235
|
if (NON_SOURCE_ROOT_DIRS.has(topLevel)) return {};
|
|
2851
3236
|
if (!relative.includes("/")) {
|
|
2852
|
-
const basename =
|
|
3237
|
+
const basename = path32.basename(filename);
|
|
2853
3238
|
if (CONFIG_FILE.test(basename) || DECLARATION_FILE.test(basename) || basename.startsWith(".")) return {};
|
|
2854
3239
|
}
|
|
2855
3240
|
return {
|
|
@@ -2911,7 +3296,7 @@ var docKindSuffixRule = {
|
|
|
2911
3296
|
};
|
|
2912
3297
|
|
|
2913
3298
|
// eslint/pasika/rules/md/title-matches-file-name.ts
|
|
2914
|
-
import
|
|
3299
|
+
import path33 from "path";
|
|
2915
3300
|
function toExpectedFileName(title) {
|
|
2916
3301
|
return `${title.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}.md`;
|
|
2917
3302
|
}
|
|
@@ -2931,7 +3316,7 @@ var titleMatchesFileNameRule = {
|
|
|
2931
3316
|
if (!filename.endsWith(".md")) return;
|
|
2932
3317
|
const title = getTextContent(node).trim();
|
|
2933
3318
|
const expectedFileName = toExpectedFileName(title);
|
|
2934
|
-
const actualFileName =
|
|
3319
|
+
const actualFileName = path33.basename(filename);
|
|
2935
3320
|
if (!title) {
|
|
2936
3321
|
context.report({
|
|
2937
3322
|
node,
|
|
@@ -3404,7 +3789,7 @@ var referenceBlockHeadingsRule = {
|
|
|
3404
3789
|
};
|
|
3405
3790
|
|
|
3406
3791
|
// eslint/pasika/rules/md/support-document-placement.ts
|
|
3407
|
-
import
|
|
3792
|
+
import path34 from "path";
|
|
3408
3793
|
var supportDocumentPlacementRule = {
|
|
3409
3794
|
meta: {
|
|
3410
3795
|
type: "problem",
|
|
@@ -3418,7 +3803,7 @@ var supportDocumentPlacementRule = {
|
|
|
3418
3803
|
root(node) {
|
|
3419
3804
|
const filename = getFilename(context);
|
|
3420
3805
|
if (!filename.endsWith(".md")) return;
|
|
3421
|
-
const parentFolder =
|
|
3806
|
+
const parentFolder = path34.basename(path34.dirname(filename));
|
|
3422
3807
|
if (filename.endsWith("-rule.md") && parentFolder !== "rules") {
|
|
3423
3808
|
context.report({
|
|
3424
3809
|
node,
|
|
@@ -3461,11 +3846,11 @@ var noTemplatePromptRule = {
|
|
|
3461
3846
|
};
|
|
3462
3847
|
|
|
3463
3848
|
// eslint/pasika/rules/md/guide-folder-entry-point.ts
|
|
3464
|
-
import
|
|
3849
|
+
import path36 from "path";
|
|
3465
3850
|
|
|
3466
3851
|
// eslint/pasika/rules/md/project-index.ts
|
|
3467
3852
|
import { readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
|
|
3468
|
-
import
|
|
3853
|
+
import path35 from "path";
|
|
3469
3854
|
var KIND_BY_SUFFIX = [
|
|
3470
3855
|
["-rule.md", "rule"],
|
|
3471
3856
|
["-guide.md", "guide"],
|
|
@@ -3474,7 +3859,7 @@ var KIND_BY_SUFFIX = [
|
|
|
3474
3859
|
];
|
|
3475
3860
|
function listMarkdownFiles(dir) {
|
|
3476
3861
|
return readdirSync2(dir).flatMap((entry) => {
|
|
3477
|
-
const entryPath =
|
|
3862
|
+
const entryPath = path35.join(dir, entry);
|
|
3478
3863
|
if (statSync3(entryPath).isDirectory()) {
|
|
3479
3864
|
return entry.startsWith("_") ? [] : listMarkdownFiles(entryPath);
|
|
3480
3865
|
}
|
|
@@ -3492,11 +3877,11 @@ function getProjectDocs(docsRoot) {
|
|
|
3492
3877
|
if (cached) return cached;
|
|
3493
3878
|
const files = listMarkdownFiles(docsRoot);
|
|
3494
3879
|
const docs = files.sort((a, b) => a.localeCompare(b)).map((filePath) => {
|
|
3495
|
-
const fileName =
|
|
3880
|
+
const fileName = path35.basename(filePath);
|
|
3496
3881
|
const kind = KIND_BY_SUFFIX.find(([suffix]) => fileName.endsWith(suffix))?.[1];
|
|
3497
3882
|
return {
|
|
3498
3883
|
filePath,
|
|
3499
|
-
doc:
|
|
3884
|
+
doc: path35.relative(docsRoot, filePath).split(path35.sep).join("/"),
|
|
3500
3885
|
fileName,
|
|
3501
3886
|
kind,
|
|
3502
3887
|
title: extractTitle(filePath)
|
|
@@ -3506,12 +3891,12 @@ function getProjectDocs(docsRoot) {
|
|
|
3506
3891
|
return docs;
|
|
3507
3892
|
}
|
|
3508
3893
|
function findDocsRoot(filePath) {
|
|
3509
|
-
let dir =
|
|
3894
|
+
let dir = path35.dirname(filePath);
|
|
3510
3895
|
for (; ; ) {
|
|
3511
|
-
if (
|
|
3896
|
+
if (path35.basename(dir) === "docs" && statSync3(dir).isDirectory()) {
|
|
3512
3897
|
return dir;
|
|
3513
3898
|
}
|
|
3514
|
-
const parent =
|
|
3899
|
+
const parent = path35.dirname(dir);
|
|
3515
3900
|
if (parent === dir) return void 0;
|
|
3516
3901
|
dir = parent;
|
|
3517
3902
|
}
|
|
@@ -3535,13 +3920,13 @@ var guideFolderEntryPointRule = {
|
|
|
3535
3920
|
if (!docsRoot) return;
|
|
3536
3921
|
const docs = getProjectDocs(docsRoot);
|
|
3537
3922
|
const guideFolders = new Set(
|
|
3538
|
-
docs.filter((doc) => ["rules", "references"].includes(
|
|
3923
|
+
docs.filter((doc) => ["rules", "references"].includes(path36.basename(path36.dirname(doc.filePath)))).map((doc) => path36.dirname(path36.dirname(doc.filePath))).filter((folder) => path36.resolve(folder) !== path36.resolve(docsRoot))
|
|
3539
3924
|
);
|
|
3540
|
-
const currentDir =
|
|
3925
|
+
const currentDir = path36.dirname(filename);
|
|
3541
3926
|
if (guideFolders.has(currentDir)) {
|
|
3542
|
-
const expectedEntryPoint = `${
|
|
3927
|
+
const expectedEntryPoint = `${path36.basename(currentDir)}.md`;
|
|
3543
3928
|
const hasEntryPoint = docs.some(
|
|
3544
|
-
(doc) => doc.kind === "guide" &&
|
|
3929
|
+
(doc) => doc.kind === "guide" && path36.dirname(doc.filePath) === currentDir && doc.fileName === expectedEntryPoint
|
|
3545
3930
|
);
|
|
3546
3931
|
if (!hasEntryPoint) {
|
|
3547
3932
|
context.report({
|
|
@@ -3598,6 +3983,39 @@ var rfcOnlyInBulletsRule = {
|
|
|
3598
3983
|
}
|
|
3599
3984
|
};
|
|
3600
3985
|
|
|
3986
|
+
// eslint/pasika/rules/md/rfc-keyword-in-every-bullet.ts
|
|
3987
|
+
function checkBullets(node, context) {
|
|
3988
|
+
if (node.type === "listItem" && !containsRfcKeyword(getTextContent(node))) {
|
|
3989
|
+
context.report({
|
|
3990
|
+
node,
|
|
3991
|
+
message: "bullet point contains no RFC 2119 keyword"
|
|
3992
|
+
});
|
|
3993
|
+
}
|
|
3994
|
+
if ("children" in node) {
|
|
3995
|
+
for (const child of node.children) {
|
|
3996
|
+
checkBullets(child, context);
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
var rfcKeywordInEveryBulletRule = {
|
|
4001
|
+
meta: {
|
|
4002
|
+
type: "problem",
|
|
4003
|
+
docs: {
|
|
4004
|
+
description: "Every bullet in a rule or policy document must contain an RFC 2119 keyword.",
|
|
4005
|
+
recommended: true
|
|
4006
|
+
}
|
|
4007
|
+
},
|
|
4008
|
+
create(context) {
|
|
4009
|
+
return {
|
|
4010
|
+
root(node) {
|
|
4011
|
+
const filename = getFilename(context);
|
|
4012
|
+
if (!filename.endsWith("-rule.md") && !filename.endsWith("-policy.md")) return;
|
|
4013
|
+
checkBullets(node, context);
|
|
4014
|
+
}
|
|
4015
|
+
};
|
|
4016
|
+
}
|
|
4017
|
+
};
|
|
4018
|
+
|
|
3601
4019
|
// eslint/pasika/rules/md/policy-subject-headings.ts
|
|
3602
4020
|
var policySubjectHeadingsRule = {
|
|
3603
4021
|
meta: {
|
|
@@ -3733,7 +4151,7 @@ var noNestedHowToRule = {
|
|
|
3733
4151
|
|
|
3734
4152
|
// eslint/pasika/rules/md/glossary-term-linking.ts
|
|
3735
4153
|
import { readFileSync as readFileSync5 } from "fs";
|
|
3736
|
-
import
|
|
4154
|
+
import path37 from "path";
|
|
3737
4155
|
function extractGlossaryTerms(filePath) {
|
|
3738
4156
|
const content = readFileSync5(filePath, "utf8");
|
|
3739
4157
|
const terms = [];
|
|
@@ -3787,9 +4205,9 @@ var glossaryTermLinkingRule = {
|
|
|
3787
4205
|
const docsRoot = findDocsRoot(filename);
|
|
3788
4206
|
if (!docsRoot) return;
|
|
3789
4207
|
const docs = getProjectDocs(docsRoot);
|
|
3790
|
-
const guideDir =
|
|
4208
|
+
const guideDir = path37.dirname(filename);
|
|
3791
4209
|
const guideReferences = docs.filter(
|
|
3792
|
-
(doc) => doc.kind === "reference" &&
|
|
4210
|
+
(doc) => doc.kind === "reference" && path37.dirname(doc.filePath) === guideDir
|
|
3793
4211
|
);
|
|
3794
4212
|
if (guideReferences.length === 0) return;
|
|
3795
4213
|
const glossaryTerms = [];
|
|
@@ -3833,6 +4251,7 @@ var mdRules = {
|
|
|
3833
4251
|
"no-template-prompt": noTemplatePromptRule,
|
|
3834
4252
|
"guide-folder-entry-point": guideFolderEntryPointRule,
|
|
3835
4253
|
"rfc-only-in-bullets": rfcOnlyInBulletsRule,
|
|
4254
|
+
"rfc-keyword-in-every-bullet": rfcKeywordInEveryBulletRule,
|
|
3836
4255
|
"policy-subject-headings": policySubjectHeadingsRule,
|
|
3837
4256
|
"guide-link-anchors": guideLinkAnchorsRule,
|
|
3838
4257
|
"no-nested-how-to": noNestedHowToRule,
|
|
@@ -3862,7 +4281,7 @@ function atrules(node) {
|
|
|
3862
4281
|
function atrulesNamed(node, name) {
|
|
3863
4282
|
return atrules(node).filter((candidate) => candidate.type === "Atrule" && candidate.name === name);
|
|
3864
4283
|
}
|
|
3865
|
-
function
|
|
4284
|
+
function blockChildren2(node) {
|
|
3866
4285
|
if (!node) return [];
|
|
3867
4286
|
if (!("block" in node) || !node.block) return [];
|
|
3868
4287
|
return node.block.children;
|
|
@@ -3909,7 +4328,7 @@ var themeResetRule = {
|
|
|
3909
4328
|
return {
|
|
3910
4329
|
"StyleSheet:exit"(node) {
|
|
3911
4330
|
const resetFound = atrulesNamed(node, "theme").some(
|
|
3912
|
-
(theme) =>
|
|
4331
|
+
(theme) => blockChildren2(theme).some((child) => {
|
|
3913
4332
|
if (child.type === "Declaration") return child.property.startsWith("--*");
|
|
3914
4333
|
if (child.type === "Raw") return child.value.includes("--*: initial");
|
|
3915
4334
|
return false;
|
|
@@ -3940,7 +4359,7 @@ var rootVariablesRule = {
|
|
|
3940
4359
|
"StyleSheet:exit"(node) {
|
|
3941
4360
|
const hasRootVars = rules(node).some((rule) => {
|
|
3942
4361
|
if (!selectorNames(rule).includes("root")) return false;
|
|
3943
|
-
return
|
|
4362
|
+
return blockChildren2(rule).some((child) => child.type === "Declaration");
|
|
3944
4363
|
});
|
|
3945
4364
|
if (!hasRootVars) {
|
|
3946
4365
|
context.report({
|
|
@@ -3949,7 +4368,7 @@ var rootVariablesRule = {
|
|
|
3949
4368
|
});
|
|
3950
4369
|
}
|
|
3951
4370
|
const referencingThemes = atrulesNamed(node, "theme").filter(
|
|
3952
|
-
(theme) =>
|
|
4371
|
+
(theme) => blockChildren2(theme).some((child) => {
|
|
3953
4372
|
if (child.type === "Raw") return child.value.includes("var(");
|
|
3954
4373
|
if (child.type === "Declaration") return JSON.stringify(child.value).includes("var(");
|
|
3955
4374
|
return false;
|
|
@@ -3983,9 +4402,9 @@ var applyUsageRule = {
|
|
|
3983
4402
|
"StyleSheet:exit"(node) {
|
|
3984
4403
|
const layers = node.children.filter((child) => child.type === "Atrule" && child.name === "layer");
|
|
3985
4404
|
for (const layer of layers) {
|
|
3986
|
-
for (const child of
|
|
4405
|
+
for (const child of blockChildren2(layer)) {
|
|
3987
4406
|
if (child.type !== "Rule") continue;
|
|
3988
|
-
for (const declaration of
|
|
4407
|
+
for (const declaration of blockChildren2(child)) {
|
|
3989
4408
|
if (declaration.type !== "Declaration") continue;
|
|
3990
4409
|
context.report({
|
|
3991
4410
|
node: declaration,
|
|
@@ -4015,10 +4434,10 @@ var baseLayerPairRule = {
|
|
|
4015
4434
|
if (baseLayers.length === 0) return;
|
|
4016
4435
|
const applied = [];
|
|
4017
4436
|
for (const layer of baseLayers) {
|
|
4018
|
-
for (const child of
|
|
4437
|
+
for (const child of blockChildren2(layer)) {
|
|
4019
4438
|
if (child.type !== "Rule") continue;
|
|
4020
4439
|
if (!selectorNames(child).includes("body")) continue;
|
|
4021
|
-
for (const declaration of
|
|
4440
|
+
for (const declaration of blockChildren2(child)) {
|
|
4022
4441
|
if (declaration.type !== "Atrule" || declaration.name !== "apply") continue;
|
|
4023
4442
|
applied.push(...preludeIdentifiers(declaration));
|
|
4024
4443
|
}
|
|
@@ -4143,7 +4562,7 @@ var customUtilityApplyRule = {
|
|
|
4143
4562
|
return {
|
|
4144
4563
|
"StyleSheet:exit"(node) {
|
|
4145
4564
|
for (const utility of atrulesNamed(node, "utility")) {
|
|
4146
|
-
for (const child of
|
|
4565
|
+
for (const child of blockChildren2(utility)) {
|
|
4147
4566
|
if (child.type === "Atrule" && child.name === "apply") continue;
|
|
4148
4567
|
if (child.type === "Declaration") {
|
|
4149
4568
|
context.report({
|
|
@@ -4215,7 +4634,7 @@ var themeVariableNamespaceRule = {
|
|
|
4215
4634
|
"StyleSheet:exit"(node) {
|
|
4216
4635
|
const namespaces = /* @__PURE__ */ new Set();
|
|
4217
4636
|
for (const theme of atrulesNamed(node, "theme")) {
|
|
4218
|
-
for (const child of
|
|
4637
|
+
for (const child of blockChildren2(theme)) {
|
|
4219
4638
|
if (child.type !== "Declaration") continue;
|
|
4220
4639
|
const segments = child.property.replace(/^--/, "").split("-");
|
|
4221
4640
|
if (segments[0]) namespaces.add(segments[0]);
|
|
@@ -4394,7 +4813,10 @@ var pasikaRules = {
|
|
|
4394
4813
|
"stay-flat": stayFlatRule,
|
|
4395
4814
|
"type-extraction": typeExtractionRule,
|
|
4396
4815
|
"locale-placement": localePlacementRule,
|
|
4816
|
+
"sole-state-owner": soleStateOwnerRule,
|
|
4817
|
+
"locale-key-shape": localeKeyShapeRule,
|
|
4397
4818
|
"shared-style-dedup": sharedStyleDedupRule,
|
|
4819
|
+
"repeated-structure": repeatedStructureRule,
|
|
4398
4820
|
"no-eslint-disable": noEslintDisableRule,
|
|
4399
4821
|
"zod-schema-validation": zodSchemaValidationRule,
|
|
4400
4822
|
"source-under-src": sourceUnderSrcRule
|