/** * `components/unknown-prop` — fires when JSX uses a prop that isn't in the * component's declared prop schema and the global `jsx.unknownProps.forbid` * policy is active. Universal React/DOM props (data-*, aria-*, on*, key, ref, * className, style, etc.) are exempt. * * The rule only fires for components we have prop metadata for. If the * component has no `prop_metadata` facts (e.g., third-party imports we don't * model yet), we cannot tell whether a prop is unknown and the rule stays * silent. */ import type { FactIndex, FactId } from "../facts/index.js"; import { makeFinding } from "./finding.js"; import type { Finding } from "./types.js"; import { indexComponentByNodeId, indexPropsByNodeId, isUniversalJsxProp, readUsageNode, } from "./utils.js"; export const RULE_ID = "components/unknown-prop"; export const RULE_VERSION = "1"; export function ruleComponentsUnknownProp(ix: FactIndex): Finding[] { const policy = ix.policy.jsxUnknownPropsForbidden(); if (!policy) return []; const findings: Finding[] = []; const componentByNode = indexComponentByNodeId(ix); const propsByNode = indexPropsByNodeId(ix); for (const [nodeId, propUsages] of propsByNode) { const usageComponent = componentByNode.get(nodeId); if (!usageComponent) continue; const knownProps = ix.components.propsOf(usageComponent.componentId); if (knownProps.length === 0) continue; const knownNames = new Set(knownProps.map((p) => p.prop)); const node = readUsageNode(ix, nodeId); if (!node) continue; for (const propUsage of propUsages) { if (propUsage.resolution === "spread") continue; if (isUniversalJsxProp(propUsage.prop)) continue; if (knownNames.has(propUsage.prop)) continue; findings.push( makeFinding({ ruleId: RULE_ID, ruleVersion: RULE_VERSION, severity: policy.severity, message: `Unknown prop "${propUsage.prop}" on <${node.element}>. Allowed props: ${formatPropList(knownNames)}.`, location: node.location, evidence: ix.evidence([node.id, usageComponent.id, propUsage.id, policy.id]), fingerprintIdentity: { file: node.file, componentId: usageComponent.componentId, element: node.element, nodePath: node.nodePath, prop: propUsage.prop, } satisfies UnknownPropFingerprint, attributes: { componentId: usageComponent.componentId, prop: propUsage.prop, }, }) ); } } return findings; } interface UnknownPropFingerprint { file: string; componentId: string; element: string; nodePath: string; prop: string; } function formatPropList(names: Set): string { if (names.size === 0) return "(none)"; const sorted = [...names].sort(); if (sorted.length <= 6) return sorted.join(", "); return `${sorted.slice(0, 6).join(", ")}, …`; } // Re-exported for type-tests / tooling that wants to read the rule's id. export type { FactId };