@sarj/eslint-plugin 2.12.0 → 2.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -37,6 +37,30 @@ module.exports = __toCommonJS(index_exports);
37
37
 
38
38
  // src/rules/enforce-file-structure.ts
39
39
  var import_utils = require("@typescript-eslint/utils");
40
+
41
+ // src/rules/_paths.ts
42
+ var SCRIPT_FILE_RE = /([\\/]scripts[\\/])|(\.mjs$)/;
43
+ var STORY_FILE_RE = /\.stories\.[cm]?[jt]sx?$/i;
44
+ var GENERATED_FILE_RE = /([\\/]generated[\\/])|(\.gen\.[cm]?[jt]sx?$)|(\.generated\.[cm]?[jt]sx?$)|(\.d\.[cm]?ts$)/;
45
+ function isTestFile(filename) {
46
+ const normalized = filename.replaceAll("\\", "/");
47
+ const base = normalized.slice(normalized.lastIndexOf("/") + 1);
48
+ if (/\.(test|spec|e2e|integration)\.[cm]?[jt]sx?$/.test(base)) {
49
+ return true;
50
+ }
51
+ return /(^|\/)(tests?|__tests__|__mocks__|fixtures|e2e|integration)\//.test(normalized);
52
+ }
53
+ function isStoryFile(filename) {
54
+ return STORY_FILE_RE.test(filename);
55
+ }
56
+ function isGeneratedFile(filename, sourceText = "") {
57
+ return GENERATED_FILE_RE.test(filename.replaceAll("\\", "/")) || /@generated\b/.test(sourceText.slice(0, 1024));
58
+ }
59
+ function isScriptFile(filename) {
60
+ return SCRIPT_FILE_RE.test(filename);
61
+ }
62
+
63
+ // src/rules/enforce-file-structure.ts
40
64
  var classifyStatement = (statement) => {
41
65
  switch (statement.type) {
42
66
  case import_utils.AST_NODE_TYPES.ImportDeclaration:
@@ -73,6 +97,9 @@ var enforce_file_structure_default = import_utils.ESLintUtils.RuleCreator(
73
97
  },
74
98
  defaultOptions: [],
75
99
  create(context) {
100
+ if (isTestFile(context.filename)) {
101
+ return {};
102
+ }
76
103
  return {
77
104
  Program(node) {
78
105
  const body = node.body;
@@ -599,22 +626,6 @@ var no_enum_default = import_utils4.ESLintUtils.RuleCreator(
599
626
 
600
627
  // src/rules/no-insecure-random-id.ts
601
628
  var import_utils5 = require("@typescript-eslint/utils");
602
-
603
- // src/rules/_paths.ts
604
- var SCRIPT_FILE_RE = /([\\/]scripts[\\/])|(\.mjs$)/;
605
- function isTestFile(filename) {
606
- const normalized = filename.replaceAll("\\", "/");
607
- const base = normalized.slice(normalized.lastIndexOf("/") + 1);
608
- if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(base)) {
609
- return true;
610
- }
611
- return /(^|\/)(tests?|__tests__|__mocks__|fixtures)\//.test(normalized);
612
- }
613
- function isScriptFile(filename) {
614
- return SCRIPT_FILE_RE.test(filename);
615
- }
616
-
617
- // src/rules/no-insecure-random-id.ts
618
629
  var STRONG_SECURITY_PATTERN = /token|secret|csrf|password|passwd|apikey|api[-_]?key|nonce|salt|uuid|authid/i;
619
630
  var NON_SECURITY_ID_PATTERN = /temp|tmp|cache|correlation|request|req|trace|execution|dev|hmr|mock|test|perf|marker/i;
620
631
  var PATH_OR_DOM_MARKER = /[\\/#]|\.[A-Za-z0-9]/;
@@ -1158,6 +1169,10 @@ var no_log_only_catch_default = import_utils8.ESLintUtils.RuleCreator(
1158
1169
  // src/rules/no-raw-env.ts
1159
1170
  var import_utils9 = require("@typescript-eslint/utils");
1160
1171
  var CONFIG_FILE_RE = /(^|[\\/])[\w.-]+\.config\.[cm]?[jt]sx?$/;
1172
+ var ENV_BOUNDARY_FILE_RE = /(^|[\\/])(?:env|client-env|server-env|client-settings|server-settings)\.[cm]?[jt]sx?$/;
1173
+ function isValidatedEnvBoundary(filename, sourceText) {
1174
+ return ENV_BOUNDARY_FILE_RE.test(filename.replaceAll("\\", "/")) && /\bz\.object\s*\(/.test(sourceText) && /\.parse\s*\(/.test(sourceText);
1175
+ }
1161
1176
  function isProcessEnv(node) {
1162
1177
  return !node.computed && node.object.type === "Identifier" && node.object.name === "process" && node.property.type === "Identifier" && node.property.name === "env";
1163
1178
  }
@@ -1207,7 +1222,7 @@ var no_raw_env_default = import_utils9.ESLintUtils.RuleCreator(
1207
1222
  defaultOptions: [],
1208
1223
  create(context) {
1209
1224
  const filename = context.filename;
1210
- if (isTestFile(filename) || isScriptFile(filename) || CONFIG_FILE_RE.test(filename.replaceAll("\\", "/"))) {
1225
+ if (isTestFile(filename) || isScriptFile(filename) || CONFIG_FILE_RE.test(filename.replaceAll("\\", "/")) || isValidatedEnvBoundary(filename, context.sourceCode.text)) {
1211
1226
  return {};
1212
1227
  }
1213
1228
  return {
@@ -2518,6 +2533,8 @@ var prefer_schema_for_api_payload_default = import_utils16.ESLintUtils.RuleCreat
2518
2533
 
2519
2534
  // src/rules/prefer-semantic-colors.ts
2520
2535
  var import_utils17 = require("@typescript-eslint/utils");
2536
+ var import_fs = require("fs");
2537
+ var import_path = require("path");
2521
2538
 
2522
2539
  // src/rules/_tailwind.ts
2523
2540
  var tailwindBase = (token) => token.replace(/^(?:[a-z0-9-]+:)+/i, "").replace(/^!/, "");
@@ -2555,6 +2572,20 @@ var STYLE_COLOR_PROPS = /* @__PURE__ */ new Set([
2555
2572
  ]);
2556
2573
  var RAW_COLOR_VALUE_RE = new RegExp(`#[0-9a-fA-F]{3,8}\\b|\\b(?:${COLOR_FN})\\s*\\(`, "i");
2557
2574
  var STORIES_FILE_RE = /\.stories\.[cm]?[jt]sx?$/i;
2575
+ var SEMANTIC_TOKEN_RE = /--(?:background|foreground|primary|secondary|muted|accent|destructive|border|card|popover)\b|(?:bg|text|border)-(?:background|foreground|primary|secondary|muted|accent|destructive|border|card|popover)\b/;
2576
+ var DETECTION_FILES = [
2577
+ "components.json",
2578
+ "tailwind.config.js",
2579
+ "tailwind.config.cjs",
2580
+ "tailwind.config.mjs",
2581
+ "tailwind.config.ts",
2582
+ "app/globals.css",
2583
+ "src/app/globals.css",
2584
+ "src/index.css",
2585
+ "src/styles/globals.css",
2586
+ "styles/globals.css"
2587
+ ];
2588
+ var semanticTokenCache = /* @__PURE__ */ new Map();
2558
2589
  var SVG_DEFS_CONTAINERS = /* @__PURE__ */ new Set([
2559
2590
  "mask",
2560
2591
  "clipPath",
@@ -2583,6 +2614,34 @@ var isInsideSvg = (node) => {
2583
2614
  }
2584
2615
  return false;
2585
2616
  };
2617
+ var hasSemanticTokenSystem = (filename) => {
2618
+ let dir = (0, import_path.dirname)(filename);
2619
+ const root = (0, import_path.parse)(dir).root;
2620
+ for (let depth = 0; depth < 8; depth += 1) {
2621
+ const cached = semanticTokenCache.get(dir);
2622
+ if (cached !== void 0) return cached;
2623
+ let found = false;
2624
+ for (const rel of DETECTION_FILES) {
2625
+ const candidate = (0, import_path.join)(dir, rel);
2626
+ if (!(0, import_fs.existsSync)(candidate)) continue;
2627
+ if (rel === "components.json") {
2628
+ found = true;
2629
+ break;
2630
+ }
2631
+ try {
2632
+ if (SEMANTIC_TOKEN_RE.test((0, import_fs.readFileSync)(candidate, "utf8"))) {
2633
+ found = true;
2634
+ break;
2635
+ }
2636
+ } catch {
2637
+ }
2638
+ }
2639
+ semanticTokenCache.set(dir, found);
2640
+ if (found || dir === root) return found;
2641
+ dir = (0, import_path.dirname)(dir);
2642
+ }
2643
+ return false;
2644
+ };
2586
2645
  var propName = (key) => {
2587
2646
  if (key.type === import_utils17.AST_NODE_TYPES.Identifier) return key.name;
2588
2647
  if (key.type === import_utils17.AST_NODE_TYPES.Literal && typeof key.value === "string") return key.value;
@@ -2597,16 +2656,27 @@ var prefer_semantic_colors_default = import_utils17.ESLintUtils.RuleCreator(
2597
2656
  docs: {
2598
2657
  description: "Enforce design-system semantic color tokens (bg-primary, text-destructive, \u2026) over raw Tailwind palette classes (text-red-500), arbitrary color values (bg-[#fff]), and inline color literals."
2599
2658
  },
2600
- schema: [],
2659
+ schema: [
2660
+ {
2661
+ type: "object",
2662
+ additionalProperties: false,
2663
+ properties: {
2664
+ requireSemanticTokens: { type: "boolean" }
2665
+ }
2666
+ }
2667
+ ],
2601
2668
  messages: {
2602
2669
  rawPalette: "Raw palette class '{{class}}' \u2014 use a semantic token (e.g. text-foreground, bg-primary, text-destructive, bg-muted).",
2603
2670
  arbitraryColor: "Hardcoded color '{{class}}' \u2014 use a semantic token, or var(--\u2026). For charts/brand add an eslint-disable with a reason.",
2604
2671
  inlineColor: "Hardcoded color '{{value}}' \u2014 use a semantic token / CSS variable. For charts/standalone pages add an eslint-disable with a reason."
2605
2672
  }
2606
2673
  },
2607
- defaultOptions: [],
2608
- create(context) {
2674
+ defaultOptions: [{}],
2675
+ create(context, [options]) {
2609
2676
  if (STORIES_FILE_RE.test(context.filename)) return {};
2677
+ if (options?.requireSemanticTokens === true && !hasSemanticTokenSystem(context.filename)) {
2678
+ return {};
2679
+ }
2610
2680
  const reportClasses = (value, node) => {
2611
2681
  for (const token of classTokens(value)) {
2612
2682
  const base = tailwindBase(token);
@@ -2865,7 +2935,7 @@ var INPUT_TYPE_REPLACEMENTS = {
2865
2935
  radio: "RadioGroup",
2866
2936
  range: "Slider"
2867
2937
  };
2868
- var SKIPPED_INPUT_TYPES = /* @__PURE__ */ new Set(["hidden"]);
2938
+ var SKIPPED_INPUT_TYPES = /* @__PURE__ */ new Set(["file", "hidden"]);
2869
2939
  var kebabCase = (component) => component.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
2870
2940
  var literalTypeAttr = (node) => {
2871
2941
  for (const attribute of node.attributes) {
@@ -2879,8 +2949,12 @@ var literalTypeAttr = (node) => {
2879
2949
  }
2880
2950
  return null;
2881
2951
  };
2952
+ var hasFileAcceptAttr = (node) => node.attributes.some(
2953
+ (attribute) => attribute.type === import_utils19.AST_NODE_TYPES.JSXAttribute && attribute.name.type === import_utils19.AST_NODE_TYPES.JSXIdentifier && attribute.name.name === "accept"
2954
+ );
2882
2955
  var resolveInputReplacement = (node) => {
2883
2956
  const typeAttr = literalTypeAttr(node);
2957
+ if (hasFileAcceptAttr(node)) return null;
2884
2958
  if (typeAttr === null || typeAttr.kind === "dynamic") return "Input";
2885
2959
  if (SKIPPED_INPUT_TYPES.has(typeAttr.value)) return null;
2886
2960
  return INPUT_TYPE_REPLACEMENTS[typeAttr.value] ?? "Input";
@@ -2901,7 +2975,7 @@ var prefer_shadcn_default = import_utils19.ESLintUtils.RuleCreator(
2901
2975
  },
2902
2976
  defaultOptions: [],
2903
2977
  create(context) {
2904
- if (isTestFile(context.filename)) {
2978
+ if (isTestFile(context.filename) || isStoryFile(context.filename)) {
2905
2979
  return {};
2906
2980
  }
2907
2981
  return {
@@ -3219,7 +3293,7 @@ var zod_naming_convention_default = import_utils22.ESLintUtils.RuleCreator(
3219
3293
  const convention = optionsArg?.convention ?? "either";
3220
3294
  const { test, messageId } = CONVENTIONS[convention];
3221
3295
  const acceptsSchemaWord = convention !== "prefix";
3222
- if (isTestFile(context.filename)) {
3296
+ if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) {
3223
3297
  return {};
3224
3298
  }
3225
3299
  return {
@@ -5518,7 +5592,9 @@ var EQUALITY_OPERATORS = /* @__PURE__ */ new Set(["===", "!==", "==", "!="]);
5518
5592
  var SENTINEL_IDENTIFIERS = /* @__PURE__ */ new Set(["undefined", "NaN"]);
5519
5593
  var SENTINEL_WORDS = /(^|_)(SENTINEL|EMPTY|NONE|NULL|UNSET|MISSING|PLACEHOLDER|DUMMY|FAKE|EXAMPLE)(_|$)/;
5520
5594
  var SENTINEL_PREFIX_RE = /^(skip|sentinel|empty|none|missing|unset|placeholder|dummy|fake|example|noop)[A-Z]/;
5595
+ var AST_NODE_TYPE_RE = /^(?:TS|JSX)?[A-Z][A-Za-z]*(?:Signature|Keyword|Expression|Declaration|Element|Literal|Identifier)$/;
5521
5596
  function isConstantReference(identifier) {
5597
+ if (AST_NODE_TYPE_RE.test(identifier)) return true;
5522
5598
  if (isAuthSecretName(identifier) && !SENTINEL_WORDS.test(identifier)) return false;
5523
5599
  return identifier === identifier.toUpperCase() && /[A-Za-z]/.test(identifier);
5524
5600
  }
@@ -5785,6 +5861,7 @@ var GLOBAL_RECEIVERS = /* @__PURE__ */ new Set([
5785
5861
  "window",
5786
5862
  "self"
5787
5863
  ]);
5864
+ var PRESIGNED_URL_NAME_RE = /(?:pre-?signed|signed|upload|download)Url$/i;
5788
5865
  function isGlobalFetchCall(node) {
5789
5866
  const callee = node.callee;
5790
5867
  if (callee.type === "Identifier") {
@@ -5795,6 +5872,23 @@ function isGlobalFetchCall(node) {
5795
5872
  }
5796
5873
  return false;
5797
5874
  }
5875
+ function identifierLikeName(node) {
5876
+ if (node.type === "Identifier") {
5877
+ return node.name;
5878
+ }
5879
+ if (node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier") {
5880
+ return node.property.name;
5881
+ }
5882
+ return null;
5883
+ }
5884
+ function isPresignedUrlTransfer(node) {
5885
+ const first = node.arguments[0];
5886
+ if (first === void 0) {
5887
+ return false;
5888
+ }
5889
+ const name = identifierLikeName(first);
5890
+ return name !== null && PRESIGNED_URL_NAME_RE.test(name);
5891
+ }
5798
5892
  function compile(patterns) {
5799
5893
  const compiled = [];
5800
5894
  for (const pattern of patterns) {
@@ -5841,6 +5935,9 @@ var no_raw_fetch_outside_clients_default = import_utils42.ESLintUtils.RuleCreato
5841
5935
  }
5842
5936
  return {
5843
5937
  CallExpression(node) {
5938
+ if (isPresignedUrlTransfer(node)) {
5939
+ return;
5940
+ }
5844
5941
  if (isGlobalFetchCall(node)) {
5845
5942
  context.report({ node, messageId: "rawFetch" });
5846
5943
  }
@@ -6466,7 +6563,7 @@ var rules = {
6466
6563
  var plugin = {
6467
6564
  meta: {
6468
6565
  name: "@sarj/eslint-plugin",
6469
- version: "2.12.0"
6566
+ version: "2.12.1"
6470
6567
  },
6471
6568
  rules,
6472
6569
  configs: {
@@ -6491,7 +6588,7 @@ var plugin = {
6491
6588
  "@sarj/prefer-discriminated-union": "warn",
6492
6589
  "@sarj/no-comment-cruft": "warn",
6493
6590
  // Frontend / styling — distilled from frontend PR-review mining.
6494
- "@sarj/prefer-semantic-colors": "warn",
6591
+ "@sarj/prefer-semantic-colors": ["warn", { requireSemanticTokens: true }],
6495
6592
  // Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
6496
6593
  "@sarj/no-fat-try-blocks": "warn",
6497
6594
  "@sarj/no-cors-wildcard-with-credentials": "warn",
@@ -6550,7 +6647,7 @@ var plugin = {
6550
6647
  "@sarj/no-comment-cruft": "error",
6551
6648
  // Frontend / styling — distilled from frontend PR-review mining. Stylistic,
6552
6649
  // no autofix → warn (rollout should prove the FP rate before raising it).
6553
- "@sarj/prefer-semantic-colors": "error",
6650
+ "@sarj/prefer-semantic-colors": ["error", { requireSemanticTokens: true }],
6554
6651
  // Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
6555
6652
  "@sarj/no-fat-try-blocks": "error",
6556
6653
  "@sarj/no-cors-wildcard-with-credentials": "error",