'use strict';
/**
 * @license Angular v20.2.0-next.5
 * (c) 2010-2025 Google LLC. https://angular.io/
 * License: MIT
 */
'use strict';

var project_tsconfig_paths = require('./project_tsconfig_paths-ClCuNoA6.cjs');
var ts = require('typescript');
require('os');
var assert = require('assert');
var index = require('./index-DTWyGOPM.cjs');
var project_paths = require('./project_paths-0E6AzVkB.cjs');
var leading_space = require('./leading_space-D9nQ8UQC.cjs');
require('./index-Dl0zaIBS.cjs');
require('path');
require('node:path');

/**
 * Reasons why a field cannot be migrated.
 *
 * Higher values of incompatibility reasons indicate a more significant
 * incompatibility reason. Lower ones may be overridden by higher ones.
 * */
exports.FieldIncompatibilityReason = void 0;
(function (FieldIncompatibilityReason) {
    FieldIncompatibilityReason[FieldIncompatibilityReason["OverriddenByDerivedClass"] = 1] = "OverriddenByDerivedClass";
    FieldIncompatibilityReason[FieldIncompatibilityReason["RedeclaredViaDerivedClassInputsArray"] = 2] = "RedeclaredViaDerivedClassInputsArray";
    FieldIncompatibilityReason[FieldIncompatibilityReason["TypeConflictWithBaseClass"] = 3] = "TypeConflictWithBaseClass";
    FieldIncompatibilityReason[FieldIncompatibilityReason["ParentIsIncompatible"] = 4] = "ParentIsIncompatible";
    FieldIncompatibilityReason[FieldIncompatibilityReason["DerivedIsIncompatible"] = 5] = "DerivedIsIncompatible";
    FieldIncompatibilityReason[FieldIncompatibilityReason["SpyOnThatOverwritesField"] = 6] = "SpyOnThatOverwritesField";
    FieldIncompatibilityReason[FieldIncompatibilityReason["PotentiallyNarrowedInTemplateButNoSupportYet"] = 7] = "PotentiallyNarrowedInTemplateButNoSupportYet";
    FieldIncompatibilityReason[FieldIncompatibilityReason["SignalIncompatibleWithHostBinding"] = 8] = "SignalIncompatibleWithHostBinding";
    FieldIncompatibilityReason[FieldIncompatibilityReason["SignalInput__RequiredButNoGoodExplicitTypeExtractable"] = 9] = "SignalInput__RequiredButNoGoodExplicitTypeExtractable";
    FieldIncompatibilityReason[FieldIncompatibilityReason["SignalInput__QuestionMarkButNoGoodExplicitTypeExtractable"] = 10] = "SignalInput__QuestionMarkButNoGoodExplicitTypeExtractable";
    FieldIncompatibilityReason[FieldIncompatibilityReason["SignalQueries__QueryListProblematicFieldAccessed"] = 11] = "SignalQueries__QueryListProblematicFieldAccessed";
    FieldIncompatibilityReason[FieldIncompatibilityReason["SignalQueries__IncompatibleMultiUnionType"] = 12] = "SignalQueries__IncompatibleMultiUnionType";
    FieldIncompatibilityReason[FieldIncompatibilityReason["WriteAssignment"] = 13] = "WriteAssignment";
    FieldIncompatibilityReason[FieldIncompatibilityReason["Accessor"] = 14] = "Accessor";
    FieldIncompatibilityReason[FieldIncompatibilityReason["OutsideOfMigrationScope"] = 15] = "OutsideOfMigrationScope";
    FieldIncompatibilityReason[FieldIncompatibilityReason["SkippedViaConfigFilter"] = 16] = "SkippedViaConfigFilter";
})(exports.FieldIncompatibilityReason || (exports.FieldIncompatibilityReason = {}));
/** Field reasons that cannot be ignored. */
const nonIgnorableFieldIncompatibilities = [
    // Outside of scope fields should not be migrated. E.g. references to inputs in `node_modules/`.
    exports.FieldIncompatibilityReason.OutsideOfMigrationScope,
    // Explicitly filtered fields cannot be skipped via best effort mode.
    exports.FieldIncompatibilityReason.SkippedViaConfigFilter,
    // There is no good output for accessor fields.
    exports.FieldIncompatibilityReason.Accessor,
    // There is no good output for such inputs. We can't perform "conversion".
    exports.FieldIncompatibilityReason.SignalInput__RequiredButNoGoodExplicitTypeExtractable,
    exports.FieldIncompatibilityReason.SignalInput__QuestionMarkButNoGoodExplicitTypeExtractable,
];
/** Reasons why a whole class and its fields cannot be migrated. */
exports.ClassIncompatibilityReason = void 0;
(function (ClassIncompatibilityReason) {
    ClassIncompatibilityReason[ClassIncompatibilityReason["ClassManuallyInstantiated"] = 0] = "ClassManuallyInstantiated";
    ClassIncompatibilityReason[ClassIncompatibilityReason["OwningClassReferencedInClassProperty"] = 1] = "OwningClassReferencedInClassProperty";
})(exports.ClassIncompatibilityReason || (exports.ClassIncompatibilityReason = {}));
/** Whether the given value refers to an field incompatibility. */
function isFieldIncompatibility(value) {
    return (value.reason !== undefined &&
        value.context !== undefined &&
        exports.FieldIncompatibilityReason.hasOwnProperty(value.reason));
}
/** Picks the more significant field compatibility. */
function pickFieldIncompatibility(a, b) {
    if (b === null) {
        return a;
    }
    if (a.reason < b.reason) {
        return b;
    }
    return a;
}

/**
 * Detects `spyOn(dirInstance, 'myInput')` calls that likely modify
 * the input signal. There is no way to change the value inside the input signal,
 * and hence observing is not possible.
 */
class SpyOnFieldPattern {
    checker;
    fields;
    constructor(checker, fields) {
        this.checker = checker;
        this.fields = fields;
    }
    detect(node) {
        if (ts.isCallExpression(node) &&
            ts.isIdentifier(node.expression) &&
            node.expression.text === 'spyOn' &&
            node.arguments.length === 2 &&
            ts.isStringLiteralLike(node.arguments[1])) {
            const spyTargetType = this.checker.getTypeAtLocation(node.arguments[0]);
            const spyProperty = spyTargetType.getProperty(node.arguments[1].text);
            if (spyProperty === undefined) {
                return;
            }
            const fieldTarget = this.fields.attemptRetrieveDescriptorFromSymbol(spyProperty);
            if (fieldTarget === null) {
                return;
            }
            this.fields.markFieldIncompatible(fieldTarget, {
                reason: exports.FieldIncompatibilityReason.SpyOnThatOverwritesField,
                context: node,
            });
        }
    }
}

/**
 * Phase where problematic patterns are detected and advise
 * the migration to skip certain inputs.
 *
 * For example, detects classes that are instantiated manually. Those
 * cannot be migrated as `input()` requires an injection context.
 *
 * In addition, spying onto an input may be problematic- so we skip migrating
 * such.
 */
function checkIncompatiblePatterns(inheritanceGraph, checker, groupedTsAstVisitor, fields, getAllClassesWithKnownFields) {
    const inputClassSymbolsToClass = new Map();
    for (const knownFieldClass of getAllClassesWithKnownFields()) {
        const classSymbol = checker.getTypeAtLocation(knownFieldClass).symbol;
        assert(classSymbol != null, 'Expected a symbol to exist for the container of known field class.');
        assert(classSymbol.valueDeclaration !== undefined, 'Expected declaration to exist for known field class.');
        assert(ts.isClassDeclaration(classSymbol.valueDeclaration), 'Expected declaration to be a class.');
        // track class symbol for derived class checks.
        inputClassSymbolsToClass.set(classSymbol, classSymbol.valueDeclaration);
    }
    const spyOnPattern = new SpyOnFieldPattern(checker, fields);
    const visitor = (node) => {
        // Check for manual class instantiations.
        if (ts.isNewExpression(node) && ts.isIdentifier(project_tsconfig_paths.unwrapExpression(node.expression))) {
            let newTarget = checker.getSymbolAtLocation(project_tsconfig_paths.unwrapExpression(node.expression));
            // Plain identifier references can point to alias symbols (e.g. imports).
            if (newTarget !== undefined && newTarget.flags & ts.SymbolFlags.Alias) {
                newTarget = checker.getAliasedSymbol(newTarget);
            }
            if (newTarget && inputClassSymbolsToClass.has(newTarget)) {
                fields.markClassIncompatible(inputClassSymbolsToClass.get(newTarget), exports.ClassIncompatibilityReason.ClassManuallyInstantiated);
            }
        }
        // Detect `spyOn` problematic usages and record them.
        spyOnPattern.detect(node);
        const insidePropertyDeclaration = groupedTsAstVisitor.state.insidePropertyDeclaration;
        // Check for problematic class references inside property declarations.
        // These are likely problematic, causing type conflicts, if the containing
        // class inherits a non-input member with the same name.
        // Suddenly the derived class changes its signature, but the base class may not.
        problematicReferencesCheck: if (insidePropertyDeclaration !== null &&
            ts.isIdentifier(node) &&
            insidePropertyDeclaration.parent.heritageClauses !== undefined) {
            let newTarget = checker.getSymbolAtLocation(project_tsconfig_paths.unwrapExpression(node));
            // Plain identifier references can point to alias symbols (e.g. imports).
            if (newTarget !== undefined && newTarget.flags & ts.SymbolFlags.Alias) {
                newTarget = checker.getAliasedSymbol(newTarget);
            }
            if (newTarget && inputClassSymbolsToClass.has(newTarget)) {
                const memberName = index.getMemberName(insidePropertyDeclaration);
                if (memberName === null) {
                    break problematicReferencesCheck;
                }
                const { derivedMembers, inherited } = inheritanceGraph.checkOverlappingMembers(insidePropertyDeclaration.parent, insidePropertyDeclaration, memberName);
                // Member is not inherited, or derived.
                // Hence the reference is unproblematic and is expected to not
                // cause any type conflicts.
                if (derivedMembers.length === 0 && inherited === undefined) {
                    break problematicReferencesCheck;
                }
                fields.markClassIncompatible(inputClassSymbolsToClass.get(newTarget), exports.ClassIncompatibilityReason.OwningClassReferencedInClassProperty);
            }
        }
    };
    groupedTsAstVisitor.register(visitor);
}

/** Gets all types that are inherited (implemented or extended). */
function getInheritedTypes(node, checker) {
    if (node.heritageClauses === undefined) {
        return [];
    }
    const heritageTypes = [];
    for (const heritageClause of node.heritageClauses) {
        for (const typeNode of heritageClause.types) {
            heritageTypes.push(checker.getTypeFromTypeNode(typeNode));
        }
    }
    return heritageTypes;
}

/**
 * Inheritance graph tracks edges between classes that describe
 * heritage.
 *
 * This graph is helpful for efficient lookups whether e.g. an input
 * is overridden, or inherited etc. This is helpful when detecting
 * and propagating input incompatibility statuses.
 */
class InheritanceGraph {
    checker;
    /** Maps nodes to their parent nodes. */
    classToParents = new Map();
    /** Maps nodes to their derived nodes. */
    parentToChildren = new Map();
    /** All classes seen participating in inheritance chains. */
    allClassesInInheritance = new Set();
    constructor(checker) {
        this.checker = checker;
    }
    /** Registers a given class in the graph. */
    registerClass(clazz, parents) {
        this.classToParents.set(clazz, parents);
        this.allClassesInInheritance.add(clazz);
        for (const parent of parents) {
            this.allClassesInInheritance.add(parent);
            if (!this.parentToChildren.has(parent)) {
                this.parentToChildren.set(parent, []);
            }
            this.parentToChildren.get(parent).push(clazz);
        }
    }
    /**
     * Checks if the given class has overlapping members, either
     * inherited or derived.
     *
     * @returns Symbols of the inherited or derived members, if they exist.
     */
    checkOverlappingMembers(clazz, member, memberName) {
        const inheritedTypes = (this.classToParents.get(clazz) ?? []).map((c) => this.checker.getTypeAtLocation(c));
        const derivedLeafs = this._traceDerivedChainToLeafs(clazz).map((c) => this.checker.getTypeAtLocation(c));
        const inheritedMember = inheritedTypes
            .map((t) => t.getProperty(memberName))
            .find((m) => m !== undefined);
        const derivedMembers = derivedLeafs
            .map((t) => t.getProperty(memberName))
            // Skip members that point back to the current class element. The derived type
            // might look up back to our starting point— which we ignore.
            .filter((m) => m !== undefined && m.valueDeclaration !== member);
        return { inherited: inheritedMember, derivedMembers };
    }
    /** Gets all leaf derived classes that extend from the given class. */
    _traceDerivedChainToLeafs(clazz) {
        const queue = [clazz];
        const leafs = [];
        while (queue.length) {
            const node = queue.shift();
            if (!this.parentToChildren.has(node)) {
                if (node !== clazz) {
                    leafs.push(node);
                }
                continue;
            }
            queue.push(...this.parentToChildren.get(node));
        }
        return leafs;
    }
    /** Gets all derived classes of the given node. */
    traceDerivedClasses(clazz) {
        const queue = [clazz];
        const derived = [];
        while (queue.length) {
            const node = queue.shift();
            if (node !== clazz) {
                derived.push(node);
            }
            if (!this.parentToChildren.has(node)) {
                continue;
            }
            queue.push(...this.parentToChildren.get(node));
        }
        return derived;
    }
    /**
     * Populates the graph.
     *
     * NOTE: This is expensive and should be called with caution.
     */
    expensivePopulate(files) {
        for (const file of files) {
            const visitor = (node) => {
                if ((ts.isClassLike(node) || ts.isInterfaceDeclaration(node)) &&
                    node.heritageClauses !== undefined) {
                    const heritageTypes = getInheritedTypes(node, this.checker);
                    const parents = heritageTypes
                        // Interfaces participate in the graph and are not "value declarations".
                        // Also, symbol may be undefined for unresolvable nodes.
                        .map((t) => (t.symbol ? t.symbol.declarations?.[0] : undefined))
                        .filter((d) => d !== undefined && (ts.isClassLike(d) || ts.isInterfaceDeclaration(d)));
                    this.registerClass(node, parents);
                }
                ts.forEachChild(node, visitor);
            };
            ts.forEachChild(file, visitor);
        }
        return this;
    }
}

/**
 * Class that allows for efficient grouping of TypeScript node AST
 * traversal.
 *
 * Allows visitors to execute in a single pass when visiting all
 * children of source files.
 */
class GroupedTsAstVisitor {
    files;
    visitors = [];
    doneFns = [];
    constructor(files) {
        this.files = files;
    }
    state = {
        insidePropertyDeclaration: null,
    };
    register(visitor, done) {
        this.visitors.push(visitor);
        if (done !== undefined) {
            this.doneFns.push(done);
        }
    }
    execute() {
        const visitor = (node) => {
            for (const v of this.visitors) {
                v(node);
            }
            if (ts.isPropertyDeclaration(node)) {
                this.state.insidePropertyDeclaration = node;
                ts.forEachChild(node, visitor);
                this.state.insidePropertyDeclaration = null;
            }
            else {
                ts.forEachChild(node, visitor);
            }
        };
        for (const file of this.files) {
            ts.forEachChild(file, visitor);
        }
        for (const doneFn of this.doneFns) {
            doneFn();
        }
        this.visitors = [];
    }
}

/**
 * Phase that propagates incompatibilities to derived classes or
 * base classes. For example, consider:
 *
 * ```ts
 * class Base {
 *   bla = true;
 * }
 *
 * class Derived extends Base {
 *   @Input() bla = false;
 * }
 * ```
 *
 * Whenever we migrate `Derived`, the inheritance would fail
 * and result in a build breakage because `Base#bla` is not an Angular input.
 *
 * The logic here detects such cases and marks `bla` as incompatible. If `Derived`
 * would then have other derived classes as well, it would propagate the status.
 */
function checkInheritanceOfKnownFields(inheritanceGraph, metaRegistry, fields, opts) {
    const allInputClasses = Array.from(inheritanceGraph.allClassesInInheritance).filter((t) => ts.isClassDeclaration(t) && opts.isClassWithKnownFields(t));
    for (const inputClass of allInputClasses) {
        // Note: Class parents of `inputClass` were already checked by
        // the previous iterations (given the reverse topological sort)—
        // hence it's safe to assume that incompatibility of parent classes will
        // not change again, at a later time.
        assert(ts.isClassDeclaration(inputClass), 'Expected input graph node to be always a class.');
        const classFields = opts.getFieldsForClass(inputClass);
        const inputFieldNamesFromMetadataArray = new Set();
        // Iterate through derived class chains and determine all inputs that are overridden
        // via class metadata fields. e.g `@Component#inputs`. This is later used to mark a
        // potential similar class input as incompatible— because those cannot be migrated.
        if (metaRegistry !== null) {
            for (const derivedClasses of inheritanceGraph.traceDerivedClasses(inputClass)) {
                const derivedMeta = ts.isClassDeclaration(derivedClasses) && derivedClasses.name !== undefined
                    ? metaRegistry.getDirectiveMetadata(new project_tsconfig_paths.Reference(derivedClasses))
                    : null;
                if (derivedMeta !== null && derivedMeta.inputFieldNamesFromMetadataArray !== null) {
                    derivedMeta.inputFieldNamesFromMetadataArray.forEach((b) => inputFieldNamesFromMetadataArray.add(b));
                }
            }
        }
        // Check inheritance of every input in the given "directive class".
        inputCheck: for (const fieldDescr of classFields) {
            const inputNode = fieldDescr.node;
            const { derivedMembers, inherited } = inheritanceGraph.checkOverlappingMembers(inputClass, inputNode, index.getMemberName(inputNode));
            // If we discover a derived, input re-declared via class metadata, then it
            // will cause conflicts as we cannot migrate it/ nor mark it as signal-based.
            if (fieldDescr.node.name !== undefined &&
                (ts.isIdentifier(fieldDescr.node.name) || ts.isStringLiteralLike(fieldDescr.node.name)) &&
                inputFieldNamesFromMetadataArray.has(fieldDescr.node.name.text)) {
                fields.captureUnknownDerivedField(fieldDescr);
            }
            for (const derived of derivedMembers) {
                const derivedInput = fields.attemptRetrieveDescriptorFromSymbol(derived);
                if (derivedInput !== null) {
                    // Note: We always track dependencies from the child to the parent,
                    // so skip here for now.
                    continue;
                }
                // If we discover a derived, non-input member, then it will cause
                // conflicts, and we mark the current input as incompatible.
                fields.captureUnknownDerivedField(fieldDescr);
                continue inputCheck;
            }
            // If there is no parent, we are done. Otherwise, check the parent
            // to either inherit or check the incompatibility with the inheritance.
            if (inherited === undefined) {
                continue;
            }
            const inheritedMemberInput = fields.attemptRetrieveDescriptorFromSymbol(inherited);
            // Parent is not an input, and hence will conflict..
            if (inheritedMemberInput === null) {
                fields.captureUnknownParentField(fieldDescr);
                continue;
            }
            fields.captureKnownFieldInheritanceRelationship(fieldDescr, inheritedMemberInput);
        }
    }
}

function removeFromUnionIfPossible(union, filter) {
    const filtered = union.types.filter(filter);
    if (filtered.length === union.types.length) {
        return union;
    }
    // If there is only item at this point, avoid the union structure.
    if (filtered.length === 1) {
        return filtered[0];
    }
    return ts.factory.updateUnionTypeNode(union, ts.factory.createNodeArray(filtered));
}

/**
 * Inserts a leading string for the given node, respecting
 * indentation of the given anchor node.
 *
 * Useful for inserting TODOs.
 */
function insertPrecedingLine(node, info, text) {
    const leadingSpace = leading_space.getLeadingLineWhitespaceOfNode(node);
    return new project_paths.Replacement(project_paths.projectFile(node.getSourceFile(), info), new project_paths.TextUpdate({
        position: node.getStart(),
        end: node.getStart(),
        toInsert: `${text}\n${leadingSpace}`,
    }));
}

/**
 * Cuts the given string into lines basing around the specified
 * line length limit. This function breaks the string on a per-word basis.
 */
function cutStringToLineLimit(str, limit) {
    const words = str.split(' ');
    const chunks = [];
    let chunkIdx = 0;
    while (words.length) {
        // New line if we exceed limit.
        if (chunks[chunkIdx] !== undefined && chunks[chunkIdx].length > limit) {
            chunkIdx++;
        }
        // Ensure line is initialized for the given index.
        if (chunks[chunkIdx] === undefined) {
            chunks[chunkIdx] = '';
        }
        const word = words.shift();
        const needsSpace = chunks[chunkIdx].length > 0;
        // Insert word. Add space before, if the line already contains text.
        chunks[chunkIdx] += `${needsSpace ? ' ' : ''}${word}`;
    }
    return chunks;
}

/**
 * Gets human-readable message information for the given field incompatibility.
 * This text will be used by the language service, or CLI-based migration.
 */
function getMessageForFieldIncompatibility(reason, fieldName) {
    switch (reason) {
        case exports.FieldIncompatibilityReason.Accessor:
            return {
                short: `Accessor ${fieldName.plural} cannot be migrated as they are too complex.`,
                extra: 'The migration potentially requires usage of `effect` or `computed`, but ' +
                    'the intent is unclear. The migration cannot safely migrate.',
            };
        case exports.FieldIncompatibilityReason.OverriddenByDerivedClass:
            return {
                short: `The ${fieldName.single} cannot be migrated because the field is overridden by a subclass.`,
                extra: 'The field in the subclass is not a signal, so migrating would break your build.',
            };
        case exports.FieldIncompatibilityReason.ParentIsIncompatible:
            return {
                short: `This ${fieldName.single} is inherited from a superclass, but the parent cannot be migrated.`,
                extra: 'Migrating this field would cause your build to fail.',
            };
        case exports.FieldIncompatibilityReason.DerivedIsIncompatible:
            return {
                short: `This ${fieldName.single} cannot be migrated because the field is overridden by a subclass.`,
                extra: 'The field in the subclass is incompatible for migration, so migrating this field would ' +
                    'break your build.',
            };
        case exports.FieldIncompatibilityReason.SignalIncompatibleWithHostBinding:
            return {
                short: `This ${fieldName.single} is used in combination with \`@HostBinding\` and ` +
                    `migrating would break.`,
                extra: `\`@HostBinding\` does not invoke the signal automatically and your code would ` +
                    `break after migration. Use \`host\` of \`@Directive\`/\`@Component\`for host bindings.`,
            };
        case exports.FieldIncompatibilityReason.PotentiallyNarrowedInTemplateButNoSupportYet:
            return {
                short: `This ${fieldName.single} is used in a control flow expression (e.g. \`@if\` or \`*ngIf\`) and ` +
                    'migrating would break narrowing currently.',
                extra: `In the future, Angular intends to support narrowing of signals.`,
            };
        case exports.FieldIncompatibilityReason.RedeclaredViaDerivedClassInputsArray:
            return {
                short: `The ${fieldName.single} is overridden by a subclass that cannot be migrated.`,
                extra: `The subclass overrides this ${fieldName.single} via the \`inputs\` array in @Directive/@Component. ` +
                    'Migrating the field would break your build because the subclass field cannot be a signal.',
            };
        case exports.FieldIncompatibilityReason.SignalInput__RequiredButNoGoodExplicitTypeExtractable:
            return {
                short: `Input is required, but the migration cannot determine a good type for the input.`,
                extra: 'Consider adding an explicit type to make the migration possible.',
            };
        case exports.FieldIncompatibilityReason.SignalInput__QuestionMarkButNoGoodExplicitTypeExtractable:
            return {
                short: `Input is marked with a question mark. Migration could not determine a good type for the input.`,
                extra: 'The migration needs to be able to resolve a type, so that it can include `undefined` in your type. ' +
                    'Consider adding an explicit type to make the migration possible.',
            };
        case exports.FieldIncompatibilityReason.SignalQueries__QueryListProblematicFieldAccessed:
            return {
                short: `There are references to this query that cannot be migrated automatically.`,
                extra: "For example, it's not possible to migrate `.changes` or `.dirty` trivially.",
            };
        case exports.FieldIncompatibilityReason.SignalQueries__IncompatibleMultiUnionType:
            return {
                short: `Query type is too complex to automatically migrate.`,
                extra: "The new query API doesn't allow us to migrate safely without breaking your app.",
            };
        case exports.FieldIncompatibilityReason.SkippedViaConfigFilter:
            return {
                short: `This ${fieldName.single} is not part of the current migration scope.`,
                extra: 'Skipped via migration config.',
            };
        case exports.FieldIncompatibilityReason.SpyOnThatOverwritesField:
            return {
                short: 'A jasmine `spyOn` call spies on this field. This breaks with signals.',
                extra: `Migration cannot safely migrate as "spyOn" writes to the ${fieldName.single}. ` +
                    `Signal ${fieldName.plural} are readonly.`,
            };
        case exports.FieldIncompatibilityReason.TypeConflictWithBaseClass:
            return {
                short: `This ${fieldName.single} overrides a field from a superclass, while the superclass ` +
                    `field is not migrated.`,
                extra: 'Migrating the field would break your build because of a type conflict.',
            };
        case exports.FieldIncompatibilityReason.WriteAssignment:
            return {
                short: `Your application code writes to the ${fieldName.single}. This prevents migration.`,
                extra: `Signal ${fieldName.plural} are readonly, so migrating would break your build.`,
            };
        case exports.FieldIncompatibilityReason.OutsideOfMigrationScope:
            return {
                short: `This ${fieldName.single} is not part of any source files in your project.`,
                extra: `The migration excludes ${fieldName.plural} if no source file declaring them was seen.`,
            };
    }
}
/**
 * Gets human-readable message information for the given class incompatibility.
 * This text will be used by the language service, or CLI-based migration.
 */
function getMessageForClassIncompatibility(reason, fieldName) {
    switch (reason) {
        case exports.ClassIncompatibilityReason.OwningClassReferencedInClassProperty:
            return {
                short: `Class of this ${fieldName.single} is referenced in the signature of another class.`,
                extra: 'The other class is likely typed to expect a non-migrated field, so ' +
                    'migration is skipped to not break your build.',
            };
        case exports.ClassIncompatibilityReason.ClassManuallyInstantiated:
            return {
                short: `Class of this ${fieldName.single} is manually instantiated. ` +
                    'This is discouraged and prevents migration.',
                extra: `Signal ${fieldName.plural} require a DI injection context. Manually instantiating ` +
                    'breaks this requirement in some cases, so the migration is skipped.',
            };
    }
}

/**
 * Inserts a TODO for the incompatibility blocking the given node
 * from being migrated.
 */
function insertTodoForIncompatibility(node, programInfo, incompatibility, fieldName) {
    // If a field is skipped via config filter or outside migration scope, do not
    // insert TODOs, as this could results in lots of unnecessary comments.
    if (isFieldIncompatibility(incompatibility) &&
        (incompatibility.reason === exports.FieldIncompatibilityReason.SkippedViaConfigFilter ||
            incompatibility.reason === exports.FieldIncompatibilityReason.OutsideOfMigrationScope)) {
        return [];
    }
    const message = isFieldIncompatibility(incompatibility)
        ? getMessageForFieldIncompatibility(incompatibility.reason, fieldName).short
        : getMessageForClassIncompatibility(incompatibility, fieldName).short;
    const lines = cutStringToLineLimit(message, 70);
    return [
        insertPrecedingLine(node, programInfo, `// TODO: Skipped for migration because:`),
        ...lines.map((line) => insertPrecedingLine(node, programInfo, `//  ${line}`)),
    ];
}

/** Whether the given node is a descendant of the given ancestor. */
function isNodeDescendantOf(node, ancestor) {
    while (node) {
        if (node === ancestor)
            return true;
        node = node.parent;
    }
    return false;
}

/** Symbol that can be used to mark a variable as reserved, synthetically. */
const ReservedMarker = Symbol();
/**
 * Gets whether the given identifier name is free for use in the
 * given location, avoiding shadowed variable names.
 *
 */
function isIdentifierFreeInScope(name, location) {
    const startContainer = findClosestParentLocalsContainer(location);
    assert(startContainer !== undefined, 'Expecting a locals container.');
    // Traverse up and check for potential collisions.
    let container = startContainer;
    let firstNextContainer = undefined;
    while (container !== undefined) {
        if (!isIdentifierFreeInContainer(name, container)) {
            return null;
        }
        if (firstNextContainer === undefined && container.nextContainer !== undefined) {
            firstNextContainer = container.nextContainer;
        }
        container = findClosestParentLocalsContainer(container.parent);
    }
    // Check descendent local containers to avoid shadowing variables.
    // Note that this is not strictly needed, but it's helping avoid
    // some lint errors, like TSLint's no shadowed variables.
    container = firstNextContainer;
    while (container && isNodeDescendantOf(container, startContainer)) {
        if (!isIdentifierFreeInContainer(name, container)) {
            return null;
        }
        container = container.nextContainer;
    }
    return { container: startContainer };
}
/** Finds the closest parent locals container. */
function findClosestParentLocalsContainer(node) {
    return ts.findAncestor(node, isLocalsContainer);
}
/** Whether the given identifier is free in the given locals container. */
function isIdentifierFreeInContainer(name, container) {
    if (container.locals === undefined || !container.locals.has(name)) {
        return true;
    }
    // We consider alias symbols as locals conservatively.
    // Note: This check is similar to the check by the TypeScript emitter.
    // typescript/stable/src/compiler/emitter.ts;l=5436;rcl=651008033
    const local = container.locals.get(name);
    return (local !== ReservedMarker &&
        !(local.flags & (ts.SymbolFlags.Value | ts.SymbolFlags.ExportValue | ts.SymbolFlags.Alias)));
}
/**
 * Whether the given node can contain local variables.
 *
 * Note: This is similar to TypeScript's `canHaveLocals` internal helper.
 * typescript/stable/src/compiler/utilitiesPublic.ts;l=2265;rcl=651008033
 */
function isLocalsContainer(node) {
    switch (node.kind) {
        case ts.SyntaxKind.ArrowFunction:
        case ts.SyntaxKind.Block:
        case ts.SyntaxKind.CallSignature:
        case ts.SyntaxKind.CaseBlock:
        case ts.SyntaxKind.CatchClause:
        case ts.SyntaxKind.ClassStaticBlockDeclaration:
        case ts.SyntaxKind.ConditionalType:
        case ts.SyntaxKind.Constructor:
        case ts.SyntaxKind.ConstructorType:
        case ts.SyntaxKind.ConstructSignature:
        case ts.SyntaxKind.ForStatement:
        case ts.SyntaxKind.ForInStatement:
        case ts.SyntaxKind.ForOfStatement:
        case ts.SyntaxKind.FunctionDeclaration:
        case ts.SyntaxKind.FunctionExpression:
        case ts.SyntaxKind.FunctionType:
        case ts.SyntaxKind.GetAccessor:
        case ts.SyntaxKind.IndexSignature:
        case ts.SyntaxKind.JSDocCallbackTag:
        case ts.SyntaxKind.JSDocEnumTag:
        case ts.SyntaxKind.JSDocFunctionType:
        case ts.SyntaxKind.JSDocSignature:
        case ts.SyntaxKind.JSDocTypedefTag:
        case ts.SyntaxKind.MappedType:
        case ts.SyntaxKind.MethodDeclaration:
        case ts.SyntaxKind.MethodSignature:
        case ts.SyntaxKind.ModuleDeclaration:
        case ts.SyntaxKind.SetAccessor:
        case ts.SyntaxKind.SourceFile:
        case ts.SyntaxKind.TypeAliasDeclaration:
            return true;
        default:
            return false;
    }
}

/**
 * Helper that can generate unique identifier names at a
 * given location.
 *
 * Used for generating unique names to extract input reads
 * to support narrowing.
 */
class UniqueNamesGenerator {
    fallbackSuffixes;
    constructor(fallbackSuffixes) {
        this.fallbackSuffixes = fallbackSuffixes;
    }
    generate(base, location) {
        const checkNameAndClaimIfAvailable = (name) => {
            const freeInfo = isIdentifierFreeInScope(name, location);
            if (freeInfo === null) {
                return false;
            }
            // Claim the locals to avoid conflicts with future generations.
            freeInfo.container.locals ??= new Map();
            freeInfo.container.locals.set(name, ReservedMarker);
            return true;
        };
        // Check the base name. Ideally, we'd use this one.
        if (checkNameAndClaimIfAvailable(base)) {
            return base;
        }
        // Try any of the possible suffixes.
        for (const suffix of this.fallbackSuffixes) {
            const name = `${base}${suffix}`;
            if (checkNameAndClaimIfAvailable(name)) {
                return name;
            }
        }
        // Worst case, suffix the base name with a unique number until
        // we find an available name.
        let name = null;
        let counter = 1;
        do {
            name = `${base}_${counter++}`;
        } while (!checkNameAndClaimIfAvailable(name));
        return name;
    }
}

/**
 * Creates replacements to insert the given statement as
 * first statement into the arrow function.
 *
 * The arrow function is converted to a block-based arrow function
 * that can hold multiple statements. The original expression is
 * simply returned like before.
 */
function createNewBlockToInsertVariable(node, file, toInsert) {
    const sf = node.getSourceFile();
    // For indentation, we traverse up and find the earliest statement.
    // This node is most of the time a good candidate for acceptable
    // indentation of a new block.
    const spacingNode = ts.findAncestor(node, ts.isStatement) ?? node.parent;
    const { character } = ts.getLineAndCharacterOfPosition(sf, spacingNode.getStart());
    const blockSpace = ' '.repeat(character);
    const contentSpace = ' '.repeat(character + 2);
    return [
        // Delete leading whitespace of the concise body.
        new project_paths.Replacement(file, new project_paths.TextUpdate({
            position: node.body.getFullStart(),
            end: node.body.getStart(),
            toInsert: '',
        })),
        // Insert leading block braces, and `toInsert` content.
        // Wrap the previous expression in a return now.
        new project_paths.Replacement(file, new project_paths.TextUpdate({
            position: node.body.getStart(),
            end: node.body.getStart(),
            toInsert: ` {\n${contentSpace}${toInsert}\n${contentSpace}return `,
        })),
        // Add trailing brace.
        new project_paths.Replacement(file, new project_paths.TextUpdate({
            position: node.body.getEnd(),
            end: node.body.getEnd(),
            toInsert: `;\n${blockSpace}}`,
        })),
    ];
}

/**
 * Migrates a binding element that refers to an Angular input.
 *
 * E.g. `const {myInput} = this`.
 *
 * For references in binding elements, we extract the element into a variable
 * where we unwrap the input. This ensures narrowing naturally works in subsequent
 * places, and we also don't need to detect potential aliases.
 *
 * ```ts
 *   const {myInput} = this;
 *   // turns into
 *   const {myInput: myInputValue} = this;
 *   const myInput = myInputValue();
 * ```
 */
function migrateBindingElementInputReference(tsReferencesInBindingElements, info, replacements, printer) {
    const nameGenerator = new UniqueNamesGenerator(['Input', 'Signal', 'Ref']);
    for (const reference of tsReferencesInBindingElements) {
        const bindingElement = reference.parent;
        const bindingDecl = index.getBindingElementDeclaration(bindingElement);
        const sourceFile = bindingElement.getSourceFile();
        const file = project_paths.projectFile(sourceFile, info);
        const inputFieldName = bindingElement.propertyName ?? bindingElement.name;
        assert(!ts.isObjectBindingPattern(inputFieldName) && !ts.isArrayBindingPattern(inputFieldName), 'Property of binding element cannot be another pattern.');
        const tmpName = nameGenerator.generate(reference.text, bindingElement);
        // Only use the temporary name, if really needed. A temporary name is needed if
        // the input field simply aliased via the binding element, or if the exposed identifier
        // is a string-literal like.
        const useTmpNameForInputField = !ts.isObjectBindingPattern(bindingElement.name) || !ts.isIdentifier(inputFieldName);
        const propertyName = useTmpNameForInputField ? inputFieldName : undefined;
        const exposedName = useTmpNameForInputField
            ? ts.factory.createIdentifier(tmpName)
            : inputFieldName;
        const newBindingToAccessInputField = ts.factory.updateBindingElement(bindingElement, bindingElement.dotDotDotToken, propertyName, exposedName, bindingElement.initializer);
        const temporaryVariableReplacements = insertTemporaryVariableForBindingElement(bindingDecl, file, `const ${bindingElement.name.getText()} = ${exposedName.text}();`);
        if (temporaryVariableReplacements === null) {
            console.error(`Could not migrate reference ${reference.text} in ${file.rootRelativePath}`);
            continue;
        }
        replacements.push(new project_paths.Replacement(file, new project_paths.TextUpdate({
            position: bindingElement.getStart(),
            end: bindingElement.getEnd(),
            toInsert: printer.printNode(ts.EmitHint.Unspecified, newBindingToAccessInputField, sourceFile),
        })), ...temporaryVariableReplacements);
    }
}
/**
 * Inserts the given code snippet after the given variable or
 * parameter declaration.
 *
 * If this is a parameter of an arrow function, a block may be
 * added automatically.
 */
function insertTemporaryVariableForBindingElement(expansionDecl, file, toInsert) {
    const sf = expansionDecl.getSourceFile();
    const parent = expansionDecl.parent;
    // The snippet is simply inserted after the variable declaration.
    // The other case of a variable declaration inside a catch clause is handled
    // below.
    if (ts.isVariableDeclaration(expansionDecl) && ts.isVariableDeclarationList(parent)) {
        const leadingSpaceCount = ts.getLineAndCharacterOfPosition(sf, parent.getStart()).character;
        const leadingSpace = ' '.repeat(leadingSpaceCount);
        const statement = parent.parent;
        return [
            new project_paths.Replacement(file, new project_paths.TextUpdate({
                position: statement.getEnd(),
                end: statement.getEnd(),
                toInsert: `\n${leadingSpace}${toInsert}`,
            })),
        ];
    }
    // If we are dealing with a object expansion inside a parameter of
    // a function-like declaration w/ block, add the variable as the first
    // node inside the block.
    const bodyBlock = getBodyBlockOfNode(parent);
    if (bodyBlock !== null) {
        const firstElementInBlock = bodyBlock.statements[0];
        const spaceReferenceNode = firstElementInBlock ?? bodyBlock;
        const spaceOffset = firstElementInBlock !== undefined ? 0 : 2;
        const leadingSpaceCount = ts.getLineAndCharacterOfPosition(sf, spaceReferenceNode.getStart()).character + spaceOffset;
        const leadingSpace = ' '.repeat(leadingSpaceCount);
        return [
            new project_paths.Replacement(file, new project_paths.TextUpdate({
                position: bodyBlock.getStart() + 1,
                end: bodyBlock.getStart() + 1,
                toInsert: `\n${leadingSpace}${toInsert}`,
            })),
        ];
    }
    // Other cases where we see an arrow function without a block.
    // We need to create one now.
    if (ts.isArrowFunction(parent) && !ts.isBlock(parent.body)) {
        return createNewBlockToInsertVariable(parent, file, toInsert);
    }
    return null;
}
/** Gets the body block of a given node, if available. */
function getBodyBlockOfNode(node) {
    if ((ts.isMethodDeclaration(node) ||
        ts.isFunctionDeclaration(node) ||
        ts.isGetAccessorDeclaration(node) ||
        ts.isConstructorDeclaration(node) ||
        ts.isArrowFunction(node)) &&
        node.body !== undefined &&
        ts.isBlock(node.body)) {
        return node.body;
    }
    if (ts.isCatchClause(node.parent)) {
        return node.parent.block;
    }
    return null;
}

/**
 * Whether the given node represents a control flow container boundary.
 * E.g. variables cannot be narrowed when descending into children of `node`.
 */
function isControlFlowBoundary(node) {
    return ((ts.isFunctionLike(node) && !getImmediatelyInvokedFunctionExpression(node)) ||
        node.kind === ts.SyntaxKind.ModuleBlock ||
        node.kind === ts.SyntaxKind.SourceFile ||
        node.kind === ts.SyntaxKind.PropertyDeclaration);
}
/** Determines the current flow container of a given node. */
function getControlFlowContainer(node) {
    return ts.findAncestor(node.parent, (node) => isControlFlowBoundary(node));
}
/** Checks whether the given node refers to an IIFE declaration. */
function getImmediatelyInvokedFunctionExpression(func) {
    if (func.kind === ts.SyntaxKind.FunctionExpression || func.kind === ts.SyntaxKind.ArrowFunction) {
        let prev = func;
        let parent = func.parent;
        while (parent.kind === ts.SyntaxKind.ParenthesizedExpression) {
            prev = parent;
            parent = parent.parent;
        }
        if (parent.kind === ts.SyntaxKind.CallExpression &&
            parent.expression === prev) {
            return parent;
        }
    }
    return undefined;
}

/** @internal */
var FlowFlags;
(function (FlowFlags) {
    FlowFlags[FlowFlags["Unreachable"] = 1] = "Unreachable";
    FlowFlags[FlowFlags["Start"] = 2] = "Start";
    FlowFlags[FlowFlags["BranchLabel"] = 4] = "BranchLabel";
    FlowFlags[FlowFlags["LoopLabel"] = 8] = "LoopLabel";
    FlowFlags[FlowFlags["Assignment"] = 16] = "Assignment";
    FlowFlags[FlowFlags["TrueCondition"] = 32] = "TrueCondition";
    FlowFlags[FlowFlags["FalseCondition"] = 64] = "FalseCondition";
    FlowFlags[FlowFlags["SwitchClause"] = 128] = "SwitchClause";
    FlowFlags[FlowFlags["ArrayMutation"] = 256] = "ArrayMutation";
    FlowFlags[FlowFlags["Call"] = 512] = "Call";
    FlowFlags[FlowFlags["ReduceLabel"] = 1024] = "ReduceLabel";
    FlowFlags[FlowFlags["Referenced"] = 2048] = "Referenced";
    FlowFlags[FlowFlags["Shared"] = 4096] = "Shared";
    FlowFlags[FlowFlags["Label"] = 12] = "Label";
    FlowFlags[FlowFlags["Condition"] = 96] = "Condition";
})(FlowFlags || (FlowFlags = {}));

/**
 * Traverses the graph of the TypeScript flow nodes, exploring all possible branches
 * and keeps track of interesting nodes that may contribute to "narrowing".
 *
 * This allows us to figure out which nodes may be narrowed or not, and need
 * temporary variables in the migration to allowing narrowing to continue working.
 *
 * Some resources on flow nodes by TypeScript:
 * https://effectivetypescript.com/2024/03/24/flownodes/.
 */
function traverseFlowForInterestingNodes(flow) {
    let flowDepth = 0;
    let interestingNodes = [];
    const queue = new Set([flow]);
    // Queue is evolved during iteration, and new items will be added
    // to the end of the iteration. Effectively implementing a queue
    // with deduping out of the box.
    for (const flow of queue) {
        if (++flowDepth === 2000) {
            // We have made 2000 recursive invocations. To avoid overflowing the call stack we report an
            // error and disable further control flow analysis in the containing function or module body.
            return interestingNodes;
        }
        const flags = flow.flags;
        if (flags & FlowFlags.Assignment) {
            const assignment = flow;
            queue.add(assignment.antecedent);
            if (ts.isVariableDeclaration(assignment.node)) {
                interestingNodes.push(assignment.node.name);
            }
            else if (ts.isBindingElement(assignment.node)) {
                interestingNodes.push(assignment.node.name);
            }
            else {
                interestingNodes.push(assignment.node);
            }
        }
        else if (flags & FlowFlags.Call) {
            queue.add(flow.antecedent);
            // Arguments can be narrowed using `FlowCall`s.
            // See: node_modules/typescript/stable/src/compiler/checker.ts;l=28786-28810
            interestingNodes.push(...flow.node.arguments);
        }
        else if (flags & FlowFlags.Condition) {
            queue.add(flow.antecedent);
            interestingNodes.push(flow.node);
        }
        else if (flags & FlowFlags.SwitchClause) {
            queue.add(flow.antecedent);
            // The switch expression can be narrowed, so it's an interesting node.
            interestingNodes.push(flow.node.switchStatement.expression);
        }
        else if (flags & FlowFlags.Label) {
            // simple label, a single ancestor.
            if (flow.antecedent?.length === 1) {
                queue.add(flow.antecedent[0]);
                continue;
            }
            if (flags & FlowFlags.BranchLabel) {
                // Normal branches. e.g. switch.
                for (const f of flow.antecedent ?? []) {
                    queue.add(f);
                }
            }
            else {
                // Branch for loops.
                // The first antecedent always points to the flow node before the loop
                // was entered. All other narrowing expressions, if present, are direct
                // antecedents of the starting flow node, so we only need to look at the first.
                // See: node_modules/typescript/stable/src/compiler/checker.ts;l=28108-28109
                queue.add(flow.antecedent[0]);
            }
        }
        else if (flags & FlowFlags.ArrayMutation) {
            queue.add(flow.antecedent);
            // Array mutations are never interesting for inputs, as we cannot migrate
            // assignments to inputs.
        }
        else if (flags & FlowFlags.ReduceLabel) {
            // reduce label is a try/catch re-routing.
            // visit all possible branches.
            // TODO: explore this more.
            // See: node_modules/typescript/stable/src/compiler/binder.ts;l=1636-1649.
            queue.add(flow.antecedent);
            for (const f of flow.node.antecedents) {
                queue.add(f);
            }
        }
        else if (flags & FlowFlags.Start) {
            // Note: TS itself only ever continues with parent control flows, if the pre-determined `flowContainer`
            // of the referenced is different. E.g. narrowing might decide to choose a higher flow container if we
            // reference a constant. In which case, TS allows escaping the flow container for narrowing. See:
            // http://google3/third_party/javascript/node_modules/typescript/stable/src/compiler/checker.ts;l=29399-29414;rcl=623599846.
            // and TypeScript's `narrowedConstInMethod` baseline test.
            // --> We don't need this as an input cannot be a constant!
            return interestingNodes;
        }
        else {
            break;
        }
    }
    return null;
}
/** Gets the flow node for the given node. */
function getFlowNode(node) {
    return node.flowNode ?? null;
}

/**
 * Analyzes the control flow of a list of references and returns
 * information about which nodes can be shared via a temporary variable
 * to enable narrowing.
 *
 * E.g. consider the following snippet:
 *
 * ```ts
 * someMethod() {
 *   if (this.bla) {
 *     this.bla.charAt(0);
 *   }
 * }
 * ```
 *
 * The analysis would inform the caller that `this.bla.charAt` can
 * be shared with the `this.bla` of the `if` condition.
 *
 * This is useful for the signal migration as it allows us to efficiently,
 * and minimally transform references into shared variables where needed.
 * Needed because signals are not narrowable by default, as they are functions.
 */
function analyzeControlFlow(entries, checker) {
    const result = [];
    const referenceToMetadata = new Map();
    // Prepare easy lookups for reference nodes to flow info.
    for (const [idx, entry] of entries.entries()) {
        const flowContainer = getControlFlowContainer(entry);
        referenceToMetadata.set(entry, {
            flowContainer,
            resultIndex: idx,
        });
        result.push({
            flowContainer,
            id: idx,
            originalNode: entry,
            recommendedNode: 'preserve',
        });
    }
    for (const entry of entries) {
        const { flowContainer, resultIndex } = referenceToMetadata.get(entry);
        const flowPathInterestingNodes = traverseFlowForInterestingNodes(getFlowNode(entry));
        assert(flowContainer !== null && flowPathInterestingNodes !== null, 'Expected a flow container to exist.');
        const narrowPartners = getAllMatchingReferencesInFlowPath(flowPathInterestingNodes, entry, referenceToMetadata, flowContainer, checker);
        if (narrowPartners.length !== 0) {
            connectSharedReferences(result, narrowPartners, resultIndex);
        }
    }
    return result;
}
/**
 * Iterates through all partner flow nodes and connects them so that
 * the first node will act as the share partner, while all subsequent
 * nodes will point to the share node.
 */
function connectSharedReferences(result, flowPartners, refId) {
    const refFlowContainer = result[refId].flowContainer;
    // Inside the list of flow partners (i.e. references to the same target),
    // find the node that is the first one in the flow container (via its start pos).
    let earliestPartner = null;
    let earliestPartnerId = null;
    for (const partnerId of flowPartners) {
        if (earliestPartner === null ||
            result[partnerId].originalNode.getStart() < earliestPartner.getStart()) {
            earliestPartner = result[partnerId].originalNode;
            earliestPartnerId = partnerId;
        }
    }
    assert(earliestPartner !== null, 'Expected an earliest partner to be found.');
    assert(earliestPartnerId !== null, 'Expected an earliest partner to be found.');
    // Earliest partner ID could be higher than `refId` in cyclic
    // situations like `loop` flow nodes. We need to find the minimum
    // and maximum to iterate through partners in between.
    const min = Math.min(earliestPartnerId, refId);
    const max = Math.max(earliestPartnerId, refId);
    // Then, incorporate all similar references (or flow nodes) in between
    // the reference and the earliest partner. References in between can also
    // use the shared flow node and not preserve their original reference— as
    // this would be rather unreadable and inefficient.
    const seenBlocks = new Set();
    let highestBlock = null;
    for (let i = min; i <= max; i++) {
        // Different flow container captured sequentially in result. Ignore.
        if (result[i].flowContainer !== refFlowContainer) {
            continue;
        }
        // Iterate up the block, find the highest block within the flow container.
        let current = result[i].originalNode.parent;
        while (current !== undefined) {
            if (isPotentialInsertionAncestor(current)) {
                // If we saw this block already, it is a common ancestor from another
                // partner. Check if it would be higher than the current highest block;
                // and choose it accordingly.
                if (seenBlocks.has(current)) {
                    if (highestBlock === null || current.getStart() < highestBlock.getStart()) {
                        highestBlock = current;
                    }
                    break;
                }
                seenBlocks.add(current);
            }
            current = current.parent;
        }
        if (i !== earliestPartnerId) {
            result[i].recommendedNode = earliestPartnerId;
        }
    }
    if (!highestBlock) {
        console.error(earliestPartnerId, refId, refFlowContainer.getText(), seenBlocks);
    }
    assert(highestBlock, 'Expected a block anchor to be found');
    result[earliestPartnerId].recommendedNode = highestBlock;
}
function isPotentialInsertionAncestor(node) {
    // Note: Arrow functions may not have a block, but instead use an expression
    // directly. This still signifies a "block" as we can convert the concise body
    // to a block.
    return (ts.isSourceFile(node) || ts.isBlock(node) || ts.isArrowFunction(node) || ts.isClassLike(node));
}
/**
 * Looks through the flow path and interesting nodes to determine which
 * of the potential "interesting" nodes point to the same reference.
 *
 * These nodes are then considered "partners" and will be returned via
 * their IDs (or practically their result indices).
 */
function getAllMatchingReferencesInFlowPath(flowPathInterestingNodes, reference, referenceToMetadata, restrainingFlowContainer, checker) {
    const partners = [];
    for (const flowNode of flowPathInterestingNodes) {
        // quick naive perf-optimized check to see if the flow node has a potential
        // similar reference.
        if (!flowNode.getText().includes(reference.getText())) {
            continue;
        }
        const similarRefNodeId = findSimilarReferenceNode(flowNode, reference, referenceToMetadata, restrainingFlowContainer, checker);
        if (similarRefNodeId !== null) {
            partners.push(similarRefNodeId);
        }
    }
    return partners;
}
/**
 * Checks if the given node contains an identifier that
 * matches the given reference. If so, returns its flow ID.
 */
function findSimilarReferenceNode(start, reference, referenceToMetadata, restrainingFlowContainer, checker) {
    return (ts.forEachChild(start, function visitChild(node) {
        // do not descend into control flow boundaries.
        // only references sharing the same container are relevant.
        // This is a performance optimization.
        if (isControlFlowBoundary(node)) {
            return;
        }
        // If this is not a potential matching identifier, check its children.
        if (!ts.isIdentifier(node) ||
            referenceToMetadata.get(node)?.flowContainer !== restrainingFlowContainer) {
            return ts.forEachChild(node, visitChild);
        }
        // If this refers to a different instantiation of the input reference,
        // continue looking.
        if (!isLexicalSameReference(checker, node, reference)) {
            return;
        }
        return { idx: referenceToMetadata.get(node).resultIndex };
    })?.idx ?? null);
}
/**
 * Checks whether a given identifier is lexically equivalent.
 * e.g. checks that they have similar property receiver accesses.
 */
function isLexicalSameReference(checker, sharePartner, reference) {
    const aParent = index.unwrapParent(reference.parent);
    // If the reference is not part a property access, return true. The references
    // are guaranteed symbol matches.
    if (!ts.isPropertyAccessExpression(aParent) && !ts.isElementAccessExpression(aParent)) {
        return sharePartner.text === reference.text;
    }
    // If reference parent is part of a property expression, but the share
    // partner not, then this cannot be shared.
    const bParent = index.unwrapParent(sharePartner.parent);
    if (aParent.kind !== bParent.kind) {
        return false;
    }
    const aParentExprSymbol = checker.getSymbolAtLocation(aParent.expression);
    const bParentExprSymbol = checker.getSymbolAtLocation(bParent.expression);
    return aParentExprSymbol === bParentExprSymbol;
}

function migrateStandardTsReference(tsReferencesWithNarrowing, checker, info, replacements) {
    const nameGenerator = new UniqueNamesGenerator(['Value', 'Val', 'Input']);
    // TODO: Consider checking/properly handling optional chaining and narrowing.
    for (const reference of tsReferencesWithNarrowing.values()) {
        const controlFlowResult = analyzeControlFlow(reference.accesses, checker);
        const idToSharedField = new Map();
        const isSharePartnerRef = (val) => {
            return val !== 'preserve' && typeof val !== 'number';
        };
        // Ensure we generate shared fields before reference entries.
        // This allows us to safely make use of `idToSharedField` whenever we come
        // across a referenced pointing to a share partner.
        controlFlowResult.sort((a, b) => {
            const aPriority = isSharePartnerRef(a.recommendedNode) ? 1 : 0;
            const bPriority = isSharePartnerRef(b.recommendedNode) ? 1 : 0;
            return bPriority - aPriority;
        });
        for (const { id, originalNode, recommendedNode } of controlFlowResult) {
            const sf = originalNode.getSourceFile();
            // Original node is preserved. No narrowing, and hence not shared.
            // Unwrap the signal directly.
            if (recommendedNode === 'preserve') {
                // Append `()` to unwrap the signal.
                replacements.push(new project_paths.Replacement(project_paths.projectFile(sf, info), new project_paths.TextUpdate({
                    position: originalNode.getEnd(),
                    end: originalNode.getEnd(),
                    toInsert: '()',
                })));
                continue;
            }
            // This reference is shared with a previous reference. Replace the access
            // with the temporary variable.
            if (typeof recommendedNode === 'number') {
                // Extract the shared field name.
                const toInsert = idToSharedField.get(recommendedNode);
                const replaceNode = index.traverseAccess(originalNode);
                assert(toInsert, 'no shared variable yet available');
                replacements.push(new project_paths.Replacement(project_paths.projectFile(sf, info), new project_paths.TextUpdate({
                    position: replaceNode.getStart(),
                    end: replaceNode.getEnd(),
                    toInsert,
                })));
                continue;
            }
            // Otherwise, we are creating a "shared reference" at the given node and
            // block.
            // Iterate up the original node, until we hit the "recommended block" level.
            // We then use the previous child as anchor for inserting. This allows us
            // to insert right before the first reference in the container, at the proper
            // block level— instead of always inserting at the beginning of the container.
            let parent = originalNode.parent;
            let referenceNodeInBlock = originalNode;
            while (parent !== recommendedNode) {
                referenceNodeInBlock = parent;
                parent = parent.parent;
            }
            const replaceNode = index.traverseAccess(originalNode);
            const filePath = project_paths.projectFile(sf, info);
            const initializer = `${replaceNode.getText()}()`;
            const fieldName = nameGenerator.generate(originalNode.text, referenceNodeInBlock);
            let sharedValueAccessExpr;
            let temporaryVariableStr;
            if (ts.isClassLike(recommendedNode)) {
                sharedValueAccessExpr = `this.${fieldName}`;
                temporaryVariableStr = `private readonly ${fieldName} = ${initializer};`;
            }
            else {
                sharedValueAccessExpr = fieldName;
                temporaryVariableStr = `const ${fieldName} = ${initializer};`;
            }
            idToSharedField.set(id, sharedValueAccessExpr);
            // If the common ancestor block of all shared references is an arrow function
            // without a block, convert the arrow function to a block and insert the temporary
            // variable at the beginning.
            if (ts.isArrowFunction(parent) && !ts.isBlock(parent.body)) {
                replacements.push(...createNewBlockToInsertVariable(parent, filePath, temporaryVariableStr));
            }
            else {
                const leadingSpace = ts.getLineAndCharacterOfPosition(sf, referenceNodeInBlock.getStart());
                replacements.push(new project_paths.Replacement(filePath, new project_paths.TextUpdate({
                    position: referenceNodeInBlock.getStart(),
                    end: referenceNodeInBlock.getStart(),
                    toInsert: `${temporaryVariableStr}\n${' '.repeat(leadingSpace.character)}`,
                })));
            }
            replacements.push(new project_paths.Replacement(project_paths.projectFile(sf, info), new project_paths.TextUpdate({
                position: replaceNode.getStart(),
                end: replaceNode.getEnd(),
                toInsert: sharedValueAccessExpr,
            })));
        }
    }
}

/**
 * Migrates TypeScript input references to be signal compatible.
 *
 * The phase takes care of control flow analysis and generates temporary variables
 * where needed to ensure narrowing continues to work. E.g.
 *
 * ```ts
 * someMethod() {
 *   if (this.input) {
 *     this.input.charAt(0);
 *   }
 * }
 * ```
 *
 * will be transformed into:
 *
 * ```ts
 * someMethod() {
 *   const input_1 = this.input();
 *   if (input_1) {
 *     input_1.charAt(0);
 *   }
 * }
 * ```
 */
function migrateTypeScriptReferences(host, references, checker, info) {
    const tsReferencesWithNarrowing = new Map();
    const tsReferencesInBindingElements = new Set();
    const seenIdentifiers = new WeakSet();
    for (const reference of references) {
        // This pass only deals with TS references.
        if (!index.isTsReference(reference)) {
            continue;
        }
        // Skip references to incompatible inputs.
        if (!host.shouldMigrateReferencesToField(reference.target)) {
            continue;
        }
        // Never attempt to migrate write references.
        // Those usually invalidate the target input most of the time, but in
        // best-effort mode they are not.
        if (reference.from.isWrite) {
            continue;
        }
        // Skip duplicate references. E.g. in batching.
        if (seenIdentifiers.has(reference.from.node)) {
            continue;
        }
        seenIdentifiers.add(reference.from.node);
        const targetKey = reference.target.key;
        if (reference.from.isPartOfElementBinding) {
            tsReferencesInBindingElements.add(reference.from.node);
        }
        else {
            if (!tsReferencesWithNarrowing.has(targetKey)) {
                tsReferencesWithNarrowing.set(targetKey, { accesses: [] });
            }
            tsReferencesWithNarrowing.get(targetKey).accesses.push(reference.from.node);
        }
    }
    migrateBindingElementInputReference(tsReferencesInBindingElements, info, host.replacements, host.printer);
    migrateStandardTsReference(tsReferencesWithNarrowing, checker, info, host.replacements);
}

/**
 * Migrates TypeScript "ts.Type" references. E.g.

 *  - `Partial<MyComp>` will be converted to `UnwrapSignalInputs<Partial<MyComp>>`.
      in Catalyst test files.
 */
function migrateTypeScriptTypeReferences(host, references, importManager, info) {
    const seenTypeNodes = new WeakSet();
    for (const reference of references) {
        // This pass only deals with TS input class type references.
        if (!index.isTsClassTypeReference(reference)) {
            continue;
        }
        // Skip references to classes that are not fully migrated.
        if (!host.shouldMigrateReferencesToClass(reference.target)) {
            continue;
        }
        // Skip duplicate references. E.g. in batching.
        if (seenTypeNodes.has(reference.from.node)) {
            continue;
        }
        seenTypeNodes.add(reference.from.node);
        if (reference.isPartialReference && reference.isPartOfCatalystFile) {
            assert(reference.from.node.typeArguments, 'Expected type arguments for partial reference.');
            assert(reference.from.node.typeArguments.length === 1, 'Expected an argument for reference.');
            const firstArg = reference.from.node.typeArguments[0];
            const sf = firstArg.getSourceFile();
            // Naive detection of the import. Sufficient for this test file migration.
            const catalystImport = sf.text.includes('google3/javascript/angular2/testing/catalyst/fake_async')
                ? 'google3/javascript/angular2/testing/catalyst/fake_async'
                : 'google3/javascript/angular2/testing/catalyst/async';
            const unwrapImportExpr = importManager.addImport({
                exportModuleSpecifier: catalystImport,
                exportSymbolName: 'UnwrapSignalInputs',
                requestedFile: sf,
            });
            host.replacements.push(new project_paths.Replacement(project_paths.projectFile(sf, info), new project_paths.TextUpdate({
                position: firstArg.getStart(),
                end: firstArg.getStart(),
                toInsert: `${host.printer.printNode(ts.EmitHint.Unspecified, unwrapImportExpr, sf)}<`,
            })));
            host.replacements.push(new project_paths.Replacement(project_paths.projectFile(sf, info), new project_paths.TextUpdate({ position: firstArg.getEnd(), end: firstArg.getEnd(), toInsert: '>' })));
        }
    }
}

exports.GroupedTsAstVisitor = GroupedTsAstVisitor;
exports.InheritanceGraph = InheritanceGraph;
exports.checkIncompatiblePatterns = checkIncompatiblePatterns;
exports.checkInheritanceOfKnownFields = checkInheritanceOfKnownFields;
exports.cutStringToLineLimit = cutStringToLineLimit;
exports.getMessageForClassIncompatibility = getMessageForClassIncompatibility;
exports.getMessageForFieldIncompatibility = getMessageForFieldIncompatibility;
exports.insertPrecedingLine = insertPrecedingLine;
exports.insertTodoForIncompatibility = insertTodoForIncompatibility;
exports.isFieldIncompatibility = isFieldIncompatibility;
exports.migrateTypeScriptReferences = migrateTypeScriptReferences;
exports.migrateTypeScriptTypeReferences = migrateTypeScriptTypeReferences;
exports.nonIgnorableFieldIncompatibilities = nonIgnorableFieldIncompatibilities;
exports.pickFieldIncompatibility = pickFieldIncompatibility;
exports.removeFromUnionIfPossible = removeFromUnionIfPossible;
