import type { NodePath } from "@babel/core"; import type { Scope } from "@babel/traverse"; import type { Expression, MemberExpression, TSMethodSignature, TSPropertySignature, } from "@babel/types"; import { getOr, memberName } from "../utils.js"; import { AnalysisError, SoftErrorRepository } from "./error.js"; import type { LocalManager } from "./local.js"; import { ClassFieldAnalysis, addClassFieldError } from "./class_fields.js"; import { trackMember } from "./track_member.js"; import { PreAnalysisResult } from "./pre.js"; export type PropsObjAnalysis = { hasDefaults: boolean; sites: PropsObjSite[]; props: Map; allAliases: PropAlias[]; }; export type PropAnalysis = { newAliasName?: string | undefined; defaultValue?: NodePath; sites: PropSite[]; aliases: PropAlias[]; typing?: NodePath | undefined; }; // These are mutually linked export type PropsObjSite = { path: NodePath; owner: string | undefined; decomposedAsAliases: boolean; child: PropSite | undefined; }; export type PropSite = { path: NodePath; parent: PropsObjSite; owner: string | undefined; enabled: boolean; }; export type PropAlias = { scope: Scope; localName: string; owner: string | undefined; }; /** * Detects assignments that expand `this.props` to variables, like: * * ```js * const { foo, bar } = this.props; * ``` * * or: * * ```js * const foo = this.props.foo; * const bar = this.props.bar; * ``` */ export function analyzeProps( propsObjAnalysis: ClassFieldAnalysis, defaultPropsObjAnalysis: ClassFieldAnalysis, locals: LocalManager, softErrors: SoftErrorRepository, preanalysis: PreAnalysisResult ): PropsObjAnalysis { const defaultProps = analyzeDefaultProps(defaultPropsObjAnalysis); const newObjSites: PropsObjSite[] = []; const props = new Map(); const getProp = (name: string) => getOr(props, name, () => ({ sites: [], aliases: [], })); for (const site of propsObjAnalysis.sites) { if (site.type !== "expr" || site.hasWrite) { addClassFieldError(site, softErrors); continue; } const memberAnalysis = trackMember(site.path); const parentSite: PropsObjSite = { path: site.path, owner: site.owner, decomposedAsAliases: false, child: undefined, }; if (memberAnalysis.fullyDecomposed && memberAnalysis.memberAliases) { newObjSites.push(parentSite); for (const [name, aliasing] of memberAnalysis.memberAliases) { getProp(name).aliases.push({ scope: aliasing.scope, localName: aliasing.localName, owner: site.owner, }); locals.reserveRemoval(aliasing.idPath); } parentSite.decomposedAsAliases = true; } else { if (defaultProps && !memberAnalysis.memberExpr) { addClassFieldError(site, softErrors); continue; } newObjSites.push(parentSite); if (memberAnalysis.memberExpr) { const child: PropSite = { path: memberAnalysis.memberExpr.path, parent: parentSite, owner: site.owner, // `enabled` will also be turned on later in callback analysis enabled: !!defaultProps, }; parentSite.child = child; getProp(memberAnalysis.memberExpr.name).sites.push(child); } } } for (const [name, propTyping] of preanalysis.propsEach) { getProp(name).typing = propTyping; } if (defaultProps) { for (const [name, defaultValue] of defaultProps) { getProp(name).defaultValue = defaultValue; } } const allAliases = Array.from(props.values()).flatMap((prop) => prop.aliases); return { hasDefaults: !!defaultProps, sites: newObjSites, props, allAliases, }; } export function needAlias(prop: PropAnalysis): boolean { return prop.aliases.length > 0 || prop.sites.some((s) => s.enabled); } function analyzeDefaultProps( defaultPropsAnalysis: ClassFieldAnalysis ): Map> | undefined { for (const site of defaultPropsAnalysis.sites) { if (!site.init) { throw new AnalysisError(`Invalid use of static defaultProps`); } } const defaultPropsFields = new Map>(); const init = defaultPropsAnalysis.sites.find((site) => site.init); if (!init) { return; } const init_ = init.init!; if (init_.type !== "init_value") { throw new AnalysisError("Non-analyzable defaultProps initializer"); } const initPath = init_.valuePath; if (!initPath.isObjectExpression()) { throw new AnalysisError("Non-analyzable defaultProps initializer"); } for (const fieldPath of initPath.get("properties")) { if (!fieldPath.isObjectProperty()) { throw new AnalysisError("Non-analyzable defaultProps initializer"); } const stateName = memberName(fieldPath.node); if (stateName == null) { throw new AnalysisError("Non-analyzable defaultProps initializer"); } const fieldInitPath = fieldPath.get("value"); if (!fieldInitPath.isExpression()) { throw new AnalysisError("Non-analyzable defaultProps initializer"); } defaultPropsFields.set(stateName, fieldInitPath); } return defaultPropsFields.size > 0 ? defaultPropsFields : undefined; }