diff --git a/lib/util/labelUtils.ts b/lib/util/labelUtils.ts index 5575e78..b7a4b96 100644 --- a/lib/util/labelUtils.ts +++ b/lib/util/labelUtils.ts @@ -4,229 +4,353 @@ import { elementType } from "jsx-ast-utils"; import { getPropValue } from "jsx-ast-utils"; import { getProp } from "jsx-ast-utils"; -import { hasNonEmptyProp } from "./hasNonEmptyProp"; -import { TSESLint } from "@typescript-eslint/utils"; // Assuming context comes from TSESLint +import { TSESLint } from "@typescript-eslint/utils"; import { JSXOpeningElement } from "estree-jsx"; import { TSESTree } from "@typescript-eslint/utils"; +/** + * Utility helpers for determining whether JSX controls are associated with visual labels + * via id/htmlFor/aria-labelledby/aria-describedby attributes. + * + * Supports these attribute value forms: + * - Literal strings: id="value" or id='value' + * - Expression containers: id={"value"} or id={variable} + * - Binary concatenations: id={"prefix" + suffix} + * - Template literals: id={`prefix-${variable}`} + * + * getAttributeValueInfo normalizes attributes into these canonical forms: + * - { kind: "string", raw: string, tokens: string[], exprText?: string } + * - { kind: "identifier", name: string } + * - { kind: "template", template: string } + * - { kind: "empty" | "none" } + */ + +const validIdentifierRe = /^[A-Za-z_$][A-Za-z0-9_$]*$/; + /** * Checks if the element is nested within a Label tag. - * e.g. - * - * @param {*} context - * @returns */ -const isInsideLabelTag = (context: TSESLint.RuleContext): boolean => { - return context.getAncestors().some(node => { +const isInsideLabelTag = (context: TSESLint.RuleContext): boolean => + context.getAncestors().some(node => { if (node.type !== "JSXElement") return false; const tagName = elementType(node.openingElement as unknown as JSXOpeningElement); return tagName.toLowerCase() === "label"; }); + +/** Regex patterns for matching id/htmlFor attributes in source text. */ +const idLiteralDouble = '"([^"]*)"'; +const idLiteralSingle = "'([^']*)'"; +const exprStringDouble = '\\{\\s*"([^"]*)"\\s*\\}'; +const exprStringSingle = "\\{\\s*'([^']*)'\\s*\\}"; +const exprIdentifier = "\\{\\s*([A-Za-z_$][A-Za-l0-9_$]*)\\s*\\}"; + +const idOrExprRegex = new RegExp( + `(?:${idLiteralDouble}|${idLiteralSingle}|${exprStringDouble}|${exprStringSingle}|${exprIdentifier})`, + "i" +); + +const escapeForRegExp = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +const getSourceText = (context: TSESLint.RuleContext) => (context.getSourceCode() as any).text as string; + +/** + * Extracts captured id value from regex match using idOrExprRegex. + * Returns the first non-empty capture group from positions 2-6. + */ +const extractCapturedId = (match: RegExpExecArray): string | undefined => { + return match[2] || match[3] || match[4] || match[5] || match[6] || undefined; }; /** - * Checks if there is a Label component inside the source code with a htmlFor attribute matching that of the id parameter. - * e.g. - * id=parameter, - * @param {*} idValue - * @param {*} context - * @returns boolean for match found or not. + * Evaluates constant BinaryExpression concatenations composed entirely of Literals. + * Returns the computed string value or undefined if evaluation fails. */ -const hasLabelWithHtmlForId = (idValue: string, context: TSESLint.RuleContext): boolean => { - if (idValue === "") { - return false; +const evalConstantString = (node: any): string | undefined => { + if (!node || typeof node !== "object") return undefined; + if (node.type === "Literal") { + return String(node.value); } - const sourceCode = context.getSourceCode(); - - const regex = /<(Label|label)[^>]*\bhtmlFor\b\s*=\s*["{']([^"'{}]*)["'}]/gi; - - let match; - while ((match = regex.exec(sourceCode.text)) !== null) { - // `match[2]` contains the `htmlFor` attribute value - if (match[2] === idValue) { - return true; - } + if (node.type === "BinaryExpression" && node.operator === "+") { + const left = evalConstantString(node.left); + if (left === undefined) return undefined; + const right = evalConstantString(node.right); + if (right === undefined) return undefined; + return left + right; } - return false; + return undefined; }; /** - * Checks if there is a Label component inside the source code with an id matching that of the id parameter. - * e.g. - * id=parameter, - * @param {*} idValue value of the props id e.g.