import type { FactIndex } from "../facts/index.js"; import { makeFinding } from "./finding.js"; import type { Finding } from "./types.js"; import { indexComponentByNodeId, readUsageNode } from "./utils.js"; export const RULE_ID = "components/preferred-component"; export const RULE_VERSION = "1"; export function ruleJsxPreferredComponent(ix: FactIndex): Finding[] { const policies = ix.policy.jsxComponentPreferred(); if (policies.length === 0) return []; const findings: Finding[] = []; const componentByNode = indexComponentByNodeId(ix); for (const [nodeId, usageComponent] of componentByNode) { const node = readUsageNode(ix, nodeId); if (!node) continue; if (node.element.includes(".")) continue; for (const policy of policies) { if (usageComponent.componentId !== policy.from) continue; const replacement = componentNameFromId(policy.to); if (!replacement) continue; findings.push( makeFinding({ ruleId: RULE_ID, ruleVersion: RULE_VERSION, severity: policy.severity, message: `<${node.element}> should use <${replacement}>.`, location: node.location, evidence: ix.evidence([node.id, usageComponent.id, policy.id]), fingerprintIdentity: { file: node.file, nodePath: node.nodePath, from: policy.from, to: policy.to, }, fix: { kind: "replaceComponent", title: policy.because ?? `Replace <${node.element}> with <${replacement}>`, from: node.element, to: replacement, deterministic: true, }, attributes: { componentId: usageComponent.componentId, rawValue: node.element, suggestedComponent: replacement, }, }), ); } } return findings; } function componentNameFromId(componentId: string): string | undefined { const hash = componentId.lastIndexOf("#"); const name = hash >= 0 ? componentId.slice(hash + 1) : componentId; if (!/^[A-Za-z_$][\w$]*$/.test(name)) return undefined; return name; }