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.
@@ -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 keyName = member.key?.type === "Identifier" ? member.key.name : void 0;
590
- if (!keyName) continue;
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(keyName)) {
598
+ if (variantNames.includes(keyName2)) {
599
599
  context.report({
600
600
  node,
601
- message: `Variant prop "${keyName}" duplicates CVA variant values. Use VariantProps<typeof variants> instead of manually writing union types. See docs/styling-guide/rules/component-variant-rule.md`
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 (!root) continue;
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)) {
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: `data-testid for ${component.smart ? "smart" : "dumb"} component "${component.name}" must be ${component.smart ? "PascalCase" : "kebab-case"}: expected "${expected}".`
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 keyName = member.key?.type === "Identifier" ? member.key.name : void 0;
1835
- if (!keyName || !VISUAL_OPTION_NAMES.has(keyName)) continue;
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 "${keyName}" must be defined through a cva() call rather than a manual union type. See docs/styling-guide/rules/component-variant-rule.md`
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 keyName = prop.key.type === "Identifier" ? prop.key.name : void 0;
1862
- if (keyName === "variants" && prop.value.type === "ObjectExpression") {
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 (keyName === "compoundVariants" && prop.value.type === "ArrayExpression") {
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/shared-style-dedup.ts
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 = path29.resolve("src");
2683
- const file = path29.resolve(context.filename);
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 path30 from "path";
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 = path30.resolve(context.filename);
3230
+ const filename = path32.resolve(context.filename);
2846
3231
  if (!MODULE_EXTENSION.test(filename)) return {};
2847
- const relative = path30.relative(process.cwd(), filename).replace(/\\/g, "/");
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 = path30.basename(filename);
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 path31 from "path";
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 = path31.basename(filename);
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 path32 from "path";
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 = path32.basename(path32.dirname(filename));
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 path34 from "path";
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 path33 from "path";
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 = path33.join(dir, entry);
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 = path33.basename(filePath);
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: path33.relative(docsRoot, filePath).split(path33.sep).join("/"),
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 = path33.dirname(filePath);
3894
+ let dir = path35.dirname(filePath);
3510
3895
  for (; ; ) {
3511
- if (path33.basename(dir) === "docs" && statSync3(dir).isDirectory()) {
3896
+ if (path35.basename(dir) === "docs" && statSync3(dir).isDirectory()) {
3512
3897
  return dir;
3513
3898
  }
3514
- const parent = path33.dirname(dir);
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(path34.basename(path34.dirname(doc.filePath)))).map((doc) => path34.dirname(path34.dirname(doc.filePath))).filter((folder) => path34.resolve(folder) !== path34.resolve(docsRoot))
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 = path34.dirname(filename);
3925
+ const currentDir = path36.dirname(filename);
3541
3926
  if (guideFolders.has(currentDir)) {
3542
- const expectedEntryPoint = `${path34.basename(currentDir)}.md`;
3927
+ const expectedEntryPoint = `${path36.basename(currentDir)}.md`;
3543
3928
  const hasEntryPoint = docs.some(
3544
- (doc) => doc.kind === "guide" && path34.dirname(doc.filePath) === currentDir && doc.fileName === expectedEntryPoint
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 path35 from "path";
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 = path35.dirname(filename);
4208
+ const guideDir = path37.dirname(filename);
3791
4209
  const guideReferences = docs.filter(
3792
- (doc) => doc.kind === "reference" && path35.dirname(doc.filePath) === guideDir
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 blockChildren(node) {
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) => blockChildren(theme).some((child) => {
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 blockChildren(rule).some((child) => child.type === "Declaration");
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) => blockChildren(theme).some((child) => {
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 blockChildren(layer)) {
4405
+ for (const child of blockChildren2(layer)) {
3987
4406
  if (child.type !== "Rule") continue;
3988
- for (const declaration of blockChildren(child)) {
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 blockChildren(layer)) {
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 blockChildren(child)) {
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 blockChildren(utility)) {
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 blockChildren(theme)) {
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