pasika 0.3.4 → 0.3.6

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,28 +2979,111 @@ var sharedStyleDedupRule = {
2702
2979
  }
2703
2980
  };
2704
2981
 
2705
- // eslint/pasika/rules/no-eslint-disable.ts
2706
- var noEslintDisableRule = {
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 = {
2707
3051
  meta: {
2708
3052
  schema: [],
2709
3053
  type: "problem",
2710
3054
  docs: {
2711
- description: "Forbid eslint-disable directives."
3055
+ description: "Require extracting a repeated block of elements as a named component."
2712
3056
  }
2713
3057
  },
2714
3058
  create(context) {
3059
+ if (!context.filename.endsWith(".tsx") && !context.filename.endsWith(".jsx")) return {};
2715
3060
  return {
2716
- Program() {
2717
- for (const comment of context.sourceCode.getAllComments()) {
2718
- if (!comment.value.includes("eslint-disable")) continue;
2719
- if (!comment.loc) continue;
2720
- context.report({
2721
- loc: comment.loc,
2722
- message: "Do not use eslint-disable directives; fix the reported violation instead."
2723
- });
2724
- }
3061
+ JSXElement(node) {
3062
+ checkChildren(node);
3063
+ },
3064
+ JSXFragment(node) {
3065
+ checkChildren(node);
2725
3066
  }
2726
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
+ }
2727
3087
  }
2728
3088
  };
2729
3089
 
@@ -2801,7 +3161,7 @@ var zodSchemaValidationRule = {
2801
3161
  };
2802
3162
 
2803
3163
  // eslint/pasika/rules/source-under-src.ts
2804
- import path30 from "path";
3164
+ import path32 from "path";
2805
3165
  var NON_SOURCE_ROOT_DIRS = /* @__PURE__ */ new Set([
2806
3166
  ".agents",
2807
3167
  ".cache",
@@ -2842,14 +3202,14 @@ var sourceUnderSrcRule = {
2842
3202
  }
2843
3203
  },
2844
3204
  create(context) {
2845
- const filename = path30.resolve(context.filename);
3205
+ const filename = path32.resolve(context.filename);
2846
3206
  if (!MODULE_EXTENSION.test(filename)) return {};
2847
- const relative = path30.relative(process.cwd(), filename).replace(/\\/g, "/");
3207
+ const relative = path32.relative(process.cwd(), filename).replace(/\\/g, "/");
2848
3208
  if (relative === "src" || relative.startsWith("src/")) return {};
2849
3209
  const topLevel = relative.split("/")[0] ?? "";
2850
3210
  if (NON_SOURCE_ROOT_DIRS.has(topLevel)) return {};
2851
3211
  if (!relative.includes("/")) {
2852
- const basename = path30.basename(filename);
3212
+ const basename = path32.basename(filename);
2853
3213
  if (CONFIG_FILE.test(basename) || DECLARATION_FILE.test(basename) || basename.startsWith(".")) return {};
2854
3214
  }
2855
3215
  return {
@@ -2911,7 +3271,7 @@ var docKindSuffixRule = {
2911
3271
  };
2912
3272
 
2913
3273
  // eslint/pasika/rules/md/title-matches-file-name.ts
2914
- import path31 from "path";
3274
+ import path33 from "path";
2915
3275
  function toExpectedFileName(title) {
2916
3276
  return `${title.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}.md`;
2917
3277
  }
@@ -2931,7 +3291,7 @@ var titleMatchesFileNameRule = {
2931
3291
  if (!filename.endsWith(".md")) return;
2932
3292
  const title = getTextContent(node).trim();
2933
3293
  const expectedFileName = toExpectedFileName(title);
2934
- const actualFileName = path31.basename(filename);
3294
+ const actualFileName = path33.basename(filename);
2935
3295
  if (!title) {
2936
3296
  context.report({
2937
3297
  node,
@@ -3404,7 +3764,7 @@ var referenceBlockHeadingsRule = {
3404
3764
  };
3405
3765
 
3406
3766
  // eslint/pasika/rules/md/support-document-placement.ts
3407
- import path32 from "path";
3767
+ import path34 from "path";
3408
3768
  var supportDocumentPlacementRule = {
3409
3769
  meta: {
3410
3770
  type: "problem",
@@ -3418,7 +3778,7 @@ var supportDocumentPlacementRule = {
3418
3778
  root(node) {
3419
3779
  const filename = getFilename(context);
3420
3780
  if (!filename.endsWith(".md")) return;
3421
- const parentFolder = path32.basename(path32.dirname(filename));
3781
+ const parentFolder = path34.basename(path34.dirname(filename));
3422
3782
  if (filename.endsWith("-rule.md") && parentFolder !== "rules") {
3423
3783
  context.report({
3424
3784
  node,
@@ -3461,11 +3821,11 @@ var noTemplatePromptRule = {
3461
3821
  };
3462
3822
 
3463
3823
  // eslint/pasika/rules/md/guide-folder-entry-point.ts
3464
- import path34 from "path";
3824
+ import path36 from "path";
3465
3825
 
3466
3826
  // eslint/pasika/rules/md/project-index.ts
3467
3827
  import { readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
3468
- import path33 from "path";
3828
+ import path35 from "path";
3469
3829
  var KIND_BY_SUFFIX = [
3470
3830
  ["-rule.md", "rule"],
3471
3831
  ["-guide.md", "guide"],
@@ -3474,7 +3834,7 @@ var KIND_BY_SUFFIX = [
3474
3834
  ];
3475
3835
  function listMarkdownFiles(dir) {
3476
3836
  return readdirSync2(dir).flatMap((entry) => {
3477
- const entryPath = path33.join(dir, entry);
3837
+ const entryPath = path35.join(dir, entry);
3478
3838
  if (statSync3(entryPath).isDirectory()) {
3479
3839
  return entry.startsWith("_") ? [] : listMarkdownFiles(entryPath);
3480
3840
  }
@@ -3492,11 +3852,11 @@ function getProjectDocs(docsRoot) {
3492
3852
  if (cached) return cached;
3493
3853
  const files = listMarkdownFiles(docsRoot);
3494
3854
  const docs = files.sort((a, b) => a.localeCompare(b)).map((filePath) => {
3495
- const fileName = path33.basename(filePath);
3855
+ const fileName = path35.basename(filePath);
3496
3856
  const kind = KIND_BY_SUFFIX.find(([suffix]) => fileName.endsWith(suffix))?.[1];
3497
3857
  return {
3498
3858
  filePath,
3499
- doc: path33.relative(docsRoot, filePath).split(path33.sep).join("/"),
3859
+ doc: path35.relative(docsRoot, filePath).split(path35.sep).join("/"),
3500
3860
  fileName,
3501
3861
  kind,
3502
3862
  title: extractTitle(filePath)
@@ -3506,12 +3866,12 @@ function getProjectDocs(docsRoot) {
3506
3866
  return docs;
3507
3867
  }
3508
3868
  function findDocsRoot(filePath) {
3509
- let dir = path33.dirname(filePath);
3869
+ let dir = path35.dirname(filePath);
3510
3870
  for (; ; ) {
3511
- if (path33.basename(dir) === "docs" && statSync3(dir).isDirectory()) {
3871
+ if (path35.basename(dir) === "docs" && statSync3(dir).isDirectory()) {
3512
3872
  return dir;
3513
3873
  }
3514
- const parent = path33.dirname(dir);
3874
+ const parent = path35.dirname(dir);
3515
3875
  if (parent === dir) return void 0;
3516
3876
  dir = parent;
3517
3877
  }
@@ -3535,13 +3895,13 @@ var guideFolderEntryPointRule = {
3535
3895
  if (!docsRoot) return;
3536
3896
  const docs = getProjectDocs(docsRoot);
3537
3897
  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))
3898
+ 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
3899
  );
3540
- const currentDir = path34.dirname(filename);
3900
+ const currentDir = path36.dirname(filename);
3541
3901
  if (guideFolders.has(currentDir)) {
3542
- const expectedEntryPoint = `${path34.basename(currentDir)}.md`;
3902
+ const expectedEntryPoint = `${path36.basename(currentDir)}.md`;
3543
3903
  const hasEntryPoint = docs.some(
3544
- (doc) => doc.kind === "guide" && path34.dirname(doc.filePath) === currentDir && doc.fileName === expectedEntryPoint
3904
+ (doc) => doc.kind === "guide" && path36.dirname(doc.filePath) === currentDir && doc.fileName === expectedEntryPoint
3545
3905
  );
3546
3906
  if (!hasEntryPoint) {
3547
3907
  context.report({
@@ -3598,6 +3958,39 @@ var rfcOnlyInBulletsRule = {
3598
3958
  }
3599
3959
  };
3600
3960
 
3961
+ // eslint/pasika/rules/md/rfc-keyword-in-every-bullet.ts
3962
+ function checkBullets(node, context) {
3963
+ if (node.type === "listItem" && !containsRfcKeyword(getTextContent(node))) {
3964
+ context.report({
3965
+ node,
3966
+ message: "bullet point contains no RFC 2119 keyword"
3967
+ });
3968
+ }
3969
+ if ("children" in node) {
3970
+ for (const child of node.children) {
3971
+ checkBullets(child, context);
3972
+ }
3973
+ }
3974
+ }
3975
+ var rfcKeywordInEveryBulletRule = {
3976
+ meta: {
3977
+ type: "problem",
3978
+ docs: {
3979
+ description: "Every bullet in a rule or policy document must contain an RFC 2119 keyword.",
3980
+ recommended: true
3981
+ }
3982
+ },
3983
+ create(context) {
3984
+ return {
3985
+ root(node) {
3986
+ const filename = getFilename(context);
3987
+ if (!filename.endsWith("-rule.md") && !filename.endsWith("-policy.md")) return;
3988
+ checkBullets(node, context);
3989
+ }
3990
+ };
3991
+ }
3992
+ };
3993
+
3601
3994
  // eslint/pasika/rules/md/policy-subject-headings.ts
3602
3995
  var policySubjectHeadingsRule = {
3603
3996
  meta: {
@@ -3733,7 +4126,7 @@ var noNestedHowToRule = {
3733
4126
 
3734
4127
  // eslint/pasika/rules/md/glossary-term-linking.ts
3735
4128
  import { readFileSync as readFileSync5 } from "fs";
3736
- import path35 from "path";
4129
+ import path37 from "path";
3737
4130
  function extractGlossaryTerms(filePath) {
3738
4131
  const content = readFileSync5(filePath, "utf8");
3739
4132
  const terms = [];
@@ -3787,9 +4180,9 @@ var glossaryTermLinkingRule = {
3787
4180
  const docsRoot = findDocsRoot(filename);
3788
4181
  if (!docsRoot) return;
3789
4182
  const docs = getProjectDocs(docsRoot);
3790
- const guideDir = path35.dirname(filename);
4183
+ const guideDir = path37.dirname(filename);
3791
4184
  const guideReferences = docs.filter(
3792
- (doc) => doc.kind === "reference" && path35.dirname(doc.filePath) === guideDir
4185
+ (doc) => doc.kind === "reference" && path37.dirname(doc.filePath) === guideDir
3793
4186
  );
3794
4187
  if (guideReferences.length === 0) return;
3795
4188
  const glossaryTerms = [];
@@ -3833,6 +4226,7 @@ var mdRules = {
3833
4226
  "no-template-prompt": noTemplatePromptRule,
3834
4227
  "guide-folder-entry-point": guideFolderEntryPointRule,
3835
4228
  "rfc-only-in-bullets": rfcOnlyInBulletsRule,
4229
+ "rfc-keyword-in-every-bullet": rfcKeywordInEveryBulletRule,
3836
4230
  "policy-subject-headings": policySubjectHeadingsRule,
3837
4231
  "guide-link-anchors": guideLinkAnchorsRule,
3838
4232
  "no-nested-how-to": noNestedHowToRule,
@@ -3862,7 +4256,7 @@ function atrules(node) {
3862
4256
  function atrulesNamed(node, name) {
3863
4257
  return atrules(node).filter((candidate) => candidate.type === "Atrule" && candidate.name === name);
3864
4258
  }
3865
- function blockChildren(node) {
4259
+ function blockChildren2(node) {
3866
4260
  if (!node) return [];
3867
4261
  if (!("block" in node) || !node.block) return [];
3868
4262
  return node.block.children;
@@ -3909,7 +4303,7 @@ var themeResetRule = {
3909
4303
  return {
3910
4304
  "StyleSheet:exit"(node) {
3911
4305
  const resetFound = atrulesNamed(node, "theme").some(
3912
- (theme) => blockChildren(theme).some((child) => {
4306
+ (theme) => blockChildren2(theme).some((child) => {
3913
4307
  if (child.type === "Declaration") return child.property.startsWith("--*");
3914
4308
  if (child.type === "Raw") return child.value.includes("--*: initial");
3915
4309
  return false;
@@ -3940,7 +4334,7 @@ var rootVariablesRule = {
3940
4334
  "StyleSheet:exit"(node) {
3941
4335
  const hasRootVars = rules(node).some((rule) => {
3942
4336
  if (!selectorNames(rule).includes("root")) return false;
3943
- return blockChildren(rule).some((child) => child.type === "Declaration");
4337
+ return blockChildren2(rule).some((child) => child.type === "Declaration");
3944
4338
  });
3945
4339
  if (!hasRootVars) {
3946
4340
  context.report({
@@ -3949,7 +4343,7 @@ var rootVariablesRule = {
3949
4343
  });
3950
4344
  }
3951
4345
  const referencingThemes = atrulesNamed(node, "theme").filter(
3952
- (theme) => blockChildren(theme).some((child) => {
4346
+ (theme) => blockChildren2(theme).some((child) => {
3953
4347
  if (child.type === "Raw") return child.value.includes("var(");
3954
4348
  if (child.type === "Declaration") return JSON.stringify(child.value).includes("var(");
3955
4349
  return false;
@@ -3983,9 +4377,9 @@ var applyUsageRule = {
3983
4377
  "StyleSheet:exit"(node) {
3984
4378
  const layers = node.children.filter((child) => child.type === "Atrule" && child.name === "layer");
3985
4379
  for (const layer of layers) {
3986
- for (const child of blockChildren(layer)) {
4380
+ for (const child of blockChildren2(layer)) {
3987
4381
  if (child.type !== "Rule") continue;
3988
- for (const declaration of blockChildren(child)) {
4382
+ for (const declaration of blockChildren2(child)) {
3989
4383
  if (declaration.type !== "Declaration") continue;
3990
4384
  context.report({
3991
4385
  node: declaration,
@@ -4015,10 +4409,10 @@ var baseLayerPairRule = {
4015
4409
  if (baseLayers.length === 0) return;
4016
4410
  const applied = [];
4017
4411
  for (const layer of baseLayers) {
4018
- for (const child of blockChildren(layer)) {
4412
+ for (const child of blockChildren2(layer)) {
4019
4413
  if (child.type !== "Rule") continue;
4020
4414
  if (!selectorNames(child).includes("body")) continue;
4021
- for (const declaration of blockChildren(child)) {
4415
+ for (const declaration of blockChildren2(child)) {
4022
4416
  if (declaration.type !== "Atrule" || declaration.name !== "apply") continue;
4023
4417
  applied.push(...preludeIdentifiers(declaration));
4024
4418
  }
@@ -4143,7 +4537,7 @@ var customUtilityApplyRule = {
4143
4537
  return {
4144
4538
  "StyleSheet:exit"(node) {
4145
4539
  for (const utility of atrulesNamed(node, "utility")) {
4146
- for (const child of blockChildren(utility)) {
4540
+ for (const child of blockChildren2(utility)) {
4147
4541
  if (child.type === "Atrule" && child.name === "apply") continue;
4148
4542
  if (child.type === "Declaration") {
4149
4543
  context.report({
@@ -4215,7 +4609,7 @@ var themeVariableNamespaceRule = {
4215
4609
  "StyleSheet:exit"(node) {
4216
4610
  const namespaces = /* @__PURE__ */ new Set();
4217
4611
  for (const theme of atrulesNamed(node, "theme")) {
4218
- for (const child of blockChildren(theme)) {
4612
+ for (const child of blockChildren2(theme)) {
4219
4613
  if (child.type !== "Declaration") continue;
4220
4614
  const segments = child.property.replace(/^--/, "").split("-");
4221
4615
  if (segments[0]) namespaces.add(segments[0]);
@@ -4285,43 +4679,8 @@ var cssRules = {
4285
4679
  "global-css-location": globalCssLocationRule
4286
4680
  };
4287
4681
 
4288
- // eslint/pasika/rules/json/no-cache-flag.ts
4289
- function memberName(member) {
4290
- return member.name.type === "String" ? member.name.value : member.name.name;
4291
- }
4292
- var noCacheFlagRule = {
4293
- meta: {
4294
- schema: [],
4295
- type: "problem",
4296
- docs: {
4297
- description: "Require lint scripts to not pass ESLint's --cache flag."
4298
- }
4299
- },
4300
- create(context) {
4301
- return {
4302
- Document(node) {
4303
- const root = node.body;
4304
- if (root.type !== "Object") return;
4305
- const scripts = root.members.find((member) => memberName(member) === "scripts");
4306
- if (scripts?.value.type !== "Object") return;
4307
- for (const member of scripts.value.members) {
4308
- const name = memberName(member);
4309
- if (name !== "lint" && name !== "fix") continue;
4310
- if (member.value.type !== "String") continue;
4311
- if (member.value.value.includes("--cache")) {
4312
- context.report({
4313
- node: member,
4314
- message: "Lint scripts must not pass ESLint's --cache flag because cross-file rules require every file in the run."
4315
- });
4316
- }
4317
- }
4318
- }
4319
- };
4320
- }
4321
- };
4322
-
4323
4682
  // eslint/pasika/rules/json/no-vulyk-dependency.ts
4324
- function memberName2(member) {
4683
+ function memberName(member) {
4325
4684
  return member.name.type === "String" ? member.name.value : member.name.name;
4326
4685
  }
4327
4686
  var noVulykDependencyRule = {
@@ -4338,9 +4697,9 @@ var noVulykDependencyRule = {
4338
4697
  const root = node.body;
4339
4698
  if (root.type !== "Object") return;
4340
4699
  for (const key of ["dependencies", "devDependencies"]) {
4341
- const section = root.members.find((member) => memberName2(member) === key);
4700
+ const section = root.members.find((member) => memberName(member) === key);
4342
4701
  if (section?.value.type !== "Object") continue;
4343
- const vulyk = section.value.members.find((member) => memberName2(member) === "vulyk");
4702
+ const vulyk = section.value.members.find((member) => memberName(member) === "vulyk");
4344
4703
  if (vulyk) {
4345
4704
  context.report({
4346
4705
  node: vulyk,
@@ -4355,7 +4714,6 @@ var noVulykDependencyRule = {
4355
4714
 
4356
4715
  // eslint/pasika/rules/json/index.ts
4357
4716
  var jsonRules = {
4358
- "no-cache-flag": noCacheFlagRule,
4359
4717
  "no-vulyk-dependency": noVulykDependencyRule
4360
4718
  };
4361
4719
 
@@ -4394,8 +4752,10 @@ var pasikaRules = {
4394
4752
  "stay-flat": stayFlatRule,
4395
4753
  "type-extraction": typeExtractionRule,
4396
4754
  "locale-placement": localePlacementRule,
4755
+ "sole-state-owner": soleStateOwnerRule,
4756
+ "locale-key-shape": localeKeyShapeRule,
4397
4757
  "shared-style-dedup": sharedStyleDedupRule,
4398
- "no-eslint-disable": noEslintDisableRule,
4758
+ "repeated-structure": repeatedStructureRule,
4399
4759
  "zod-schema-validation": zodSchemaValidationRule,
4400
4760
  "source-under-src": sourceUnderSrcRule
4401
4761
  };