@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 +52 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 =
|
|
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
|
-
|
|
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
|
-
|
|
7719
|
-
if (
|
|
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 (
|
|
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 =
|
|
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;
|