@decantr/verifier 3.4.0 → 3.4.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.js CHANGED
@@ -1160,13 +1160,16 @@ function isAcceptedTokenValue(value, tokenHints) {
1160
1160
  return value.includes(normalizedHint) || value.includes(`var(${normalizedHint})`);
1161
1161
  });
1162
1162
  }
1163
- function arbitraryInlineStyleValue(property, value, tokenHints) {
1163
+ function arbitraryInlineStyleValue(property, value, tokenHints, options = {}) {
1164
1164
  const normalizedProperty = property.trim();
1165
1165
  const normalizedValue = value.trim();
1166
1166
  if (!normalizedProperty || !normalizedValue) return null;
1167
1167
  const isVisualProperty = INLINE_STYLE_PROPERTIES.has(normalizedProperty) || STYLESHEET_VISUAL_PROPERTIES.has(normalizedProperty) || normalizedProperty.startsWith("--");
1168
1168
  if (!isVisualProperty) return null;
1169
1169
  if (isAcceptedTokenValue(normalizedValue, tokenHints)) return null;
1170
+ if (options.allowCssVariableValues && /\bvar\(\s*--[A-Za-z0-9_-]+/i.test(normalizedValue)) {
1171
+ return null;
1172
+ }
1170
1173
  if (/(?:^|[\s,(])#[0-9a-f]{3,8}\b|(?:rgba?|hsla?|oklch|oklab|lch|lab|color-mix)\(/i.test(
1171
1174
  normalizedValue
1172
1175
  )) {
@@ -1286,7 +1289,9 @@ function collectStylesheetBridgeDriftFindings(input) {
1286
1289
  const property = match.groups?.property;
1287
1290
  const rawValue = match.groups?.value;
1288
1291
  if (!property || !rawValue) continue;
1289
- const value = arbitraryInlineStyleValue(property, rawValue, input.bridge.tokenHints);
1292
+ const value = arbitraryInlineStyleValue(property, rawValue, input.bridge.tokenHints, {
1293
+ allowCssVariableValues: true
1294
+ });
1290
1295
  if (!value) continue;
1291
1296
  const lineNumber = lineIndex + 1;
1292
1297
  const key = `${file}:${lineNumber}:${value}`;
@@ -3941,7 +3946,8 @@ function createContractAssertions(projectRoot, audit) {
3941
3946
  if (v4) {
3942
3947
  const declaredRoutes = Object.keys(v4.blueprint.routes ?? {}).sort();
3943
3948
  for (const route of declaredRoutes) {
3944
- const routeTarget = (v4.blueprint.routes ?? {})[route];
3949
+ const routeTarget = v4.blueprint.routes?.[route];
3950
+ if (!routeTarget) continue;
3945
3951
  const section = v4.blueprint.sections.find((entry) => entry.id === routeTarget.section);
3946
3952
  const page = section?.pages.find((entry) => entry.id === routeTarget.page);
3947
3953
  assertions.push(
@@ -7666,9 +7672,15 @@ function getJsxTextContent(node) {
7666
7672
  });
7667
7673
  return text;
7668
7674
  }
7675
+ function hasNonEmptyJsxAttribute(attributes, ...names) {
7676
+ const attribute = getJsxAttribute(attributes, ...names);
7677
+ if (!attribute?.initializer) return false;
7678
+ const literalValue = getJsxAttributeLiteralValue(attribute);
7679
+ return literalValue === null || literalValue.trim().length > 0;
7680
+ }
7669
7681
  function hasAccessibleLabel(attributes, textContent) {
7670
7682
  return Boolean(
7671
- getJsxAttribute(attributes, "aria-label", "aria-labelledby", "title") || textContent.trim().length > 0
7683
+ hasNonEmptyJsxAttribute(attributes, "aria-label", "aria-labelledby", "title") || textContent.trim().length > 0
7672
7684
  );
7673
7685
  }
7674
7686
  function isNonSemanticInteractiveTag(tagName) {
@@ -7693,6 +7705,18 @@ function collectLabelForIds(root) {
7693
7705
  walk(root);
7694
7706
  return ids;
7695
7707
  }
7708
+ function collectElementIds(root) {
7709
+ const ids = /* @__PURE__ */ new Set();
7710
+ const walk = (node) => {
7711
+ if (ts6.isJsxSelfClosingElement(node) || ts6.isJsxOpeningElement(node)) {
7712
+ const idValue = getJsxAttributeLiteralValue(getJsxAttribute(node.attributes, "id"));
7713
+ if (idValue?.trim()) ids.add(idValue.trim());
7714
+ }
7715
+ ts6.forEachChild(node, walk);
7716
+ };
7717
+ walk(root);
7718
+ return ids;
7719
+ }
7696
7720
  function isWrappedInJsxLabel(node) {
7697
7721
  let current = node.parent;
7698
7722
  while (current) {
@@ -7715,21 +7739,37 @@ function hasAncestorJsxTag(node, tagName) {
7715
7739
  }
7716
7740
  function isLabelableFormControl(tagName, attributes) {
7717
7741
  if (!tagName) return false;
7718
- if (tagName === "select" || tagName === "textarea") return true;
7719
- if (tagName !== "input") return false;
7742
+ const normalizedTagName = tagName.toLowerCase();
7743
+ if (normalizedTagName === "select" || normalizedTagName === "textarea") return true;
7744
+ if (normalizedTagName !== "input" && !isLikelyProjectFormControlTag(tagName)) return false;
7720
7745
  const inputType = (getJsxAttributeLiteralValue(getJsxAttribute(attributes, "type")) ?? "text").toLowerCase();
7721
7746
  return !["hidden", "submit", "reset", "button"].includes(inputType);
7722
7747
  }
7748
+ function isLikelyProjectFormControlTag(tagName) {
7749
+ if (tagName === tagName.toLowerCase()) return false;
7750
+ const normalizedTagName = tagName.toLowerCase();
7751
+ return /(?:^|(?:text|email|password|search|number|tel|url|date))input$/.test(normalizedTagName) || /^(?:textarea|textArea|select|combobox|comboBox|checkbox|radio|switch)$/i.test(tagName);
7752
+ }
7723
7753
  function getNormalizedInputType(attributes) {
7724
7754
  return (getJsxAttributeLiteralValue(getJsxAttribute(attributes, "type")) ?? "text").trim().toLowerCase();
7725
7755
  }
7726
- function hasFormControlLabel(node, tagName, attributes, labelForIds, textContent) {
7756
+ function hasFormControlLabel(node, tagName, attributes, labelForIds, elementIds, textContent) {
7727
7757
  if (!isLabelableFormControl(tagName, attributes)) return true;
7728
- if (hasAccessibleLabel(attributes, textContent)) return true;
7758
+ if (hasNonEmptyJsxAttribute(attributes, "aria-label", "title")) return true;
7759
+ if (hasAriaLabelledByReference(attributes, elementIds)) return true;
7760
+ if (textContent.trim().length > 0) return true;
7729
7761
  if (isWrappedInJsxLabel(node)) return true;
7730
7762
  const idValue = getJsxAttributeLiteralValue(getJsxAttribute(attributes, "id"));
7731
7763
  return Boolean(idValue && labelForIds.has(idValue));
7732
7764
  }
7765
+ function hasAriaLabelledByReference(attributes, elementIds) {
7766
+ const attribute = getJsxAttribute(attributes, "aria-labelledby");
7767
+ if (!attribute?.initializer) return false;
7768
+ const literalValue = getJsxAttributeLiteralValue(attribute);
7769
+ if (literalValue === null) return true;
7770
+ const references = literalValue.trim().split(/\s+/).filter(Boolean);
7771
+ return references.some((reference) => elementIds.has(reference));
7772
+ }
7733
7773
  function isExternalLinkTargetBlankWithoutRel(attributes) {
7734
7774
  const targetValue = getJsxAttributeLiteralValue(getJsxAttribute(attributes, "target"));
7735
7775
  if (targetValue !== "_blank") return false;
@@ -9414,7 +9454,7 @@ function countAuthAnonymousRedirectSignals(code) {
9414
9454
  ];
9415
9455
  return patterns.reduce((count, pattern) => count + (pattern.test(code) ? 1 : 0), 0);
9416
9456
  }
9417
- var OPEN_REDIRECT_QUERY_KEY_PATTERN = String.raw`next|redirect(?:To|[_-]to)?|return(?:To|[_-]to)?|callback(?:Url|[_-]url)?|continue(?:Url|[_-]url)?|from`;
9457
+ var OPEN_REDIRECT_QUERY_KEY_PATTERN = "next|redirect(?:To|[_-]to)?|return(?:To|[_-]to)?|callback(?:Url|[_-]url)?|continue(?:Url|[_-]url)?|from";
9418
9458
  var OPEN_REDIRECT_SOURCE_PATTERN = String.raw`\b(?:searchParams|(?:request|req)\.nextUrl\.searchParams|url\.searchParams)\.get\s*\(\s*['"\`](?:${OPEN_REDIRECT_QUERY_KEY_PATTERN})['"\`]\s*\)|\b(?:router\.query|route\.query|query)\.(?:${OPEN_REDIRECT_QUERY_KEY_PATTERN})\b|\b(?:new\s+)?URLSearchParams\s*\(\s*(?:(?:window|globalThis|document|self|parent|top)\.)?location\.(?:search|hash)(?:\.slice\(\s*1\s*\)|\.replace\([^)]*\))?\s*\)\.get\s*\(\s*['"\`](?:${OPEN_REDIRECT_QUERY_KEY_PATTERN})['"\`]\s*\)|\bnew\s+URL\(\s*(?:request|req)\.url\s*\)\.searchParams\.get\s*\(\s*['"\`](?:${OPEN_REDIRECT_QUERY_KEY_PATTERN})['"\`]\s*\)`;
9419
9459
  var OPEN_REDIRECT_SOURCE_REGEX = new RegExp(OPEN_REDIRECT_SOURCE_PATTERN, "i");
9420
9460
  var OPEN_REDIRECT_QUERY_KEY_REGEX = new RegExp(`^(?:${OPEN_REDIRECT_QUERY_KEY_PATTERN})$`, "i");
@@ -13330,6 +13370,7 @@ function analyzeAstSignals(filePath, code) {
13330
13370
  const namedExpressionInitializers = collectNamedExpressionInitializers(sourceFile);
13331
13371
  const namedPropertyAliases = collectNamedPropertyAliases(sourceFile);
13332
13372
  const labelForIds = collectLabelForIds(sourceFile);
13373
+ const elementIds = collectElementIds(sourceFile);
13333
13374
  let navigationLandmarkCount = 0;
13334
13375
  let unlabeledNavigationLandmarkCount = 0;
13335
13376
  const walk = (node) => {
@@ -13736,7 +13777,7 @@ function analyzeAstSignals(filePath, code) {
13736
13777
  signals.authInputWithoutNameCount += 1;
13737
13778
  }
13738
13779
  }
13739
- if (!hasFormControlLabel(node, tagName, node.attributes, labelForIds, "")) {
13780
+ if (!hasFormControlLabel(node, tagName, node.attributes, labelForIds, elementIds, "")) {
13740
13781
  signals.formControlWithoutLabelCount += 1;
13741
13782
  }
13742
13783
  }
@@ -13851,6 +13892,7 @@ function analyzeAstSignals(filePath, code) {
13851
13892
  tagName,
13852
13893
  node.openingElement.attributes,
13853
13894
  labelForIds,
13895
+ elementIds,
13854
13896
  textContent
13855
13897
  )) {
13856
13898
  signals.formControlWithoutLabelCount += 1;