All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.schematics.bundles.inject-migration.js Maven / Gradle / Ivy

There is a newer version: 19.2.4
Show newest version
'use strict';
/**
 * @license Angular v19.0.5
 * (c) 2010-2024 Google LLC. https://angular.io/
 * License: MIT
 */
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var schematics = require('@angular-devkit/schematics');
var p = require('path');
var compiler_host = require('./compiler_host-82c877de.js');
var ts = require('typescript');
var nodes = require('./nodes-a9f0b985.js');
var imports = require('./imports-abe29092.js');
var leading_space = require('./leading_space-d190b83b.js');
require('./checker-eced36c5.js');
require('os');
require('fs');
require('module');
require('url');

function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }

var ts__default = /*#__PURE__*/_interopDefaultLegacy(ts);

/*!
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */
/** Names of decorators that enable DI on a class declaration. */
const DECORATORS_SUPPORTING_DI = new Set([
    'Component',
    'Directive',
    'Pipe',
    'NgModule',
    'Injectable',
]);
/** Names of symbols used for DI on parameters. */
const DI_PARAM_SYMBOLS = new Set([
    'Inject',
    'Attribute',
    'Optional',
    'SkipSelf',
    'Self',
    'Host',
    'forwardRef',
]);
/** Kinds of nodes which aren't injectable when set as a type of a parameter. */
const UNINJECTABLE_TYPE_KINDS = new Set([
    ts__default["default"].SyntaxKind.TrueKeyword,
    ts__default["default"].SyntaxKind.FalseKeyword,
    ts__default["default"].SyntaxKind.NumberKeyword,
    ts__default["default"].SyntaxKind.StringKeyword,
    ts__default["default"].SyntaxKind.NullKeyword,
    ts__default["default"].SyntaxKind.VoidKeyword,
]);
/**
 * Finds the necessary information for the `inject` migration in a file.
 * @param sourceFile File which to analyze.
 * @param localTypeChecker Type checker scoped to the specific file.
 */
function analyzeFile(sourceFile, localTypeChecker, options) {
    const coreSpecifiers = imports.getNamedImports(sourceFile, '@angular/core');
    // Exit early if there are no Angular imports.
    if (coreSpecifiers === null || coreSpecifiers.elements.length === 0) {
        return null;
    }
    const classes = [];
    const nonDecoratorReferences = {};
    const importsToSpecifiers = coreSpecifiers.elements.reduce((map, specifier) => {
        const symbolName = (specifier.propertyName || specifier.name).text;
        if (DI_PARAM_SYMBOLS.has(symbolName)) {
            map.set(symbolName, specifier);
        }
        return map;
    }, new Map());
    sourceFile.forEachChild(function walk(node) {
        // Skip import declarations since they can throw off the identifier
        // could below and we don't care about them in this migration.
        if (ts__default["default"].isImportDeclaration(node)) {
            return;
        }
        if (ts__default["default"].isParameter(node)) {
            const closestConstructor = nodes.closestNode(node, ts__default["default"].isConstructorDeclaration);
            // Visiting the same parameters that we're about to remove can throw off the reference
            // counting logic below. If we run into an initializer, we always visit its initializer
            // and optionally visit the modifiers/decorators if it's not due to be deleted. Note that
            // here we technically aren't dealing with the the full list of classes, but the parent class
            // will have been visited by the time we reach the parameters.
            if (node.initializer) {
                walk(node.initializer);
            }
            if (closestConstructor === null ||
                // This is meant to avoid the case where this is a
                // parameter inside a function placed in a constructor.
                !closestConstructor.parameters.includes(node) ||
                !classes.some((c) => c.constructor === closestConstructor)) {
                node.modifiers?.forEach(walk);
            }
            return;
        }
        if (ts__default["default"].isIdentifier(node) && importsToSpecifiers.size > 0) {
            let symbol;
            for (const [name, specifier] of importsToSpecifiers) {
                const localName = (specifier.propertyName || specifier.name).text;
                // Quick exit if the two symbols don't match up.
                if (localName === node.text) {
                    if (!symbol) {
                        symbol = localTypeChecker.getSymbolAtLocation(node);
                        // If the symbol couldn't be resolved the first time, it won't be resolved the next
                        // time either. Stop the loop since we won't be able to get an accurate result.
                        if (!symbol || !symbol.declarations) {
                            break;
                        }
                        else if (symbol.declarations.some((decl) => decl === specifier)) {
                            nonDecoratorReferences[name] = (nonDecoratorReferences[name] || 0) + 1;
                        }
                    }
                }
            }
        }
        else if (ts__default["default"].isClassDeclaration(node)) {
            const decorators = nodes.getAngularDecorators(localTypeChecker, ts__default["default"].getDecorators(node) || []);
            const isAbstract = !!node.modifiers?.some((m) => m.kind === ts__default["default"].SyntaxKind.AbstractKeyword);
            const supportsDI = decorators.some((dec) => DECORATORS_SUPPORTING_DI.has(dec.name));
            const constructorNode = node.members.find((member) => ts__default["default"].isConstructorDeclaration(member) &&
                member.body != null &&
                member.parameters.length > 0);
            // Basic check to determine if all parameters are injectable. This isn't exhaustive, but it
            // should catch the majority of cases. An exhaustive check would require a full type checker
            // which we don't have in this migration.
            const allParamsInjectable = !!constructorNode?.parameters.every((param) => {
                if (!param.type || !UNINJECTABLE_TYPE_KINDS.has(param.type.kind)) {
                    return true;
                }
                return nodes.getAngularDecorators(localTypeChecker, ts__default["default"].getDecorators(param) || []).some((dec) => dec.name === 'Inject' || dec.name === 'Attribute');
            });
            // Don't migrate abstract classes by default, because
            // their parameters aren't guaranteed to be injectable.
            if (supportsDI &&
                constructorNode &&
                allParamsInjectable &&
                (!isAbstract || options.migrateAbstractClasses)) {
                classes.push({
                    node,
                    constructor: constructorNode,
                    superCall: node.heritageClauses ? findSuperCall(constructorNode) : null,
                });
            }
        }
        node.forEachChild(walk);
    });
    return { classes, nonDecoratorReferences };
}
/**
 * Returns the parameters of a function that aren't used within its body.
 * @param declaration Function in which to search for unused parameters.
 * @param localTypeChecker Type checker scoped to the file in which the function was declared.
 * @param removedStatements Statements that were already removed from the constructor.
 */
function getConstructorUnusedParameters(declaration, localTypeChecker, removedStatements) {
    const accessedTopLevelParameters = new Set();
    const topLevelParameters = new Set();
    const topLevelParameterNames = new Set();
    const unusedParams = new Set();
    // Prepare the parameters for quicker checks further down.
    for (const param of declaration.parameters) {
        if (ts__default["default"].isIdentifier(param.name)) {
            topLevelParameters.add(param);
            topLevelParameterNames.add(param.name.text);
        }
    }
    if (!declaration.body) {
        return topLevelParameters;
    }
    const analyze = (node) => {
        // Don't descend into statements that were removed already.
        if (ts__default["default"].isStatement(node) && removedStatements.has(node)) {
            return;
        }
        if (!ts__default["default"].isIdentifier(node) || !topLevelParameterNames.has(node.text)) {
            node.forEachChild(analyze);
            return;
        }
        // Don't consider `this.` accesses as being references to
        // parameters since they'll be moved to property declarations.
        if (isAccessedViaThis(node)) {
            return;
        }
        localTypeChecker.getSymbolAtLocation(node)?.declarations?.forEach((decl) => {
            if (ts__default["default"].isParameter(decl) && topLevelParameters.has(decl)) {
                accessedTopLevelParameters.add(decl);
            }
            if (ts__default["default"].isShorthandPropertyAssignment(decl)) {
                const symbol = localTypeChecker.getShorthandAssignmentValueSymbol(decl);
                if (symbol && symbol.valueDeclaration && ts__default["default"].isParameter(symbol.valueDeclaration)) {
                    accessedTopLevelParameters.add(symbol.valueDeclaration);
                }
            }
        });
    };
    declaration.parameters.forEach((param) => {
        if (param.initializer) {
            analyze(param.initializer);
        }
    });
    declaration.body.forEachChild(analyze);
    for (const param of topLevelParameters) {
        if (!accessedTopLevelParameters.has(param)) {
            unusedParams.add(param);
        }
    }
    return unusedParams;
}
/**
 * Determines which parameters of a function declaration are used within its `super` call.
 * @param declaration Function whose parameters to search for.
 * @param superCall `super()` call within the function.
 * @param localTypeChecker Type checker scoped to the file in which the function is declared.
 */
function getSuperParameters(declaration, superCall, localTypeChecker) {
    const usedParams = new Set();
    const topLevelParameters = new Set();
    const topLevelParameterNames = new Set();
    // Prepare the parameters for quicker checks further down.
    for (const param of declaration.parameters) {
        if (ts__default["default"].isIdentifier(param.name)) {
            topLevelParameters.add(param);
            topLevelParameterNames.add(param.name.text);
        }
    }
    superCall.forEachChild(function walk(node) {
        if (ts__default["default"].isIdentifier(node) && topLevelParameterNames.has(node.text)) {
            localTypeChecker.getSymbolAtLocation(node)?.declarations?.forEach((decl) => {
                if (ts__default["default"].isParameter(decl) && topLevelParameters.has(decl)) {
                    usedParams.add(decl);
                }
            });
        }
        else {
            node.forEachChild(walk);
        }
    });
    return usedParams;
}
/**
 * Determines if a specific parameter has references to other parameters.
 * @param param Parameter to check.
 * @param allParameters All parameters of the containing function.
 * @param localTypeChecker Type checker scoped to the current file.
 */
function parameterReferencesOtherParameters(param, allParameters, localTypeChecker) {
    // A parameter can only reference other parameters through its initializer.
    if (!param.initializer || allParameters.length < 2) {
        return false;
    }
    const paramNames = new Set();
    for (const current of allParameters) {
        if (current !== param && ts__default["default"].isIdentifier(current.name)) {
            paramNames.add(current.name.text);
        }
    }
    let result = false;
    const analyze = (node) => {
        if (ts__default["default"].isIdentifier(node) && paramNames.has(node.text) && !isAccessedViaThis(node)) {
            const symbol = localTypeChecker.getSymbolAtLocation(node);
            const referencesOtherParam = symbol?.declarations?.some((decl) => {
                return allParameters.includes(decl);
            });
            if (referencesOtherParam) {
                result = true;
            }
        }
        if (!result) {
            node.forEachChild(analyze);
        }
    };
    analyze(param.initializer);
    return result;
}
/** Checks whether a parameter node declares a property on its class. */
function parameterDeclaresProperty(node) {
    return !!node.modifiers?.some(({ kind }) => kind === ts__default["default"].SyntaxKind.PublicKeyword ||
        kind === ts__default["default"].SyntaxKind.PrivateKeyword ||
        kind === ts__default["default"].SyntaxKind.ProtectedKeyword ||
        kind === ts__default["default"].SyntaxKind.ReadonlyKeyword);
}
/** Checks whether a type node is nullable. */
function isNullableType(node) {
    // Apparently `foo: null` is `Parameter>`,
    // while `foo: undefined` is `Parameter`...
    if (node.kind === ts__default["default"].SyntaxKind.UndefinedKeyword || node.kind === ts__default["default"].SyntaxKind.VoidKeyword) {
        return true;
    }
    if (ts__default["default"].isLiteralTypeNode(node)) {
        return node.literal.kind === ts__default["default"].SyntaxKind.NullKeyword;
    }
    if (ts__default["default"].isUnionTypeNode(node)) {
        return node.types.some(isNullableType);
    }
    return false;
}
/** Checks whether a type node has generic arguments. */
function hasGenerics(node) {
    if (ts__default["default"].isTypeReferenceNode(node)) {
        return node.typeArguments != null && node.typeArguments.length > 0;
    }
    if (ts__default["default"].isUnionTypeNode(node)) {
        return node.types.some(hasGenerics);
    }
    return false;
}
/** Checks whether an identifier is accessed through `this`, e.g. `this.`. */
function isAccessedViaThis(node) {
    return (ts__default["default"].isPropertyAccessExpression(node.parent) &&
        node.parent.expression.kind === ts__default["default"].SyntaxKind.ThisKeyword &&
        node.parent.name === node);
}
/** Finds a `super` call inside of a specific node. */
function findSuperCall(root) {
    let result = null;
    root.forEachChild(function find(node) {
        if (ts__default["default"].isCallExpression(node) && node.expression.kind === ts__default["default"].SyntaxKind.SuperKeyword) {
            result = node;
        }
        else if (result === null) {
            node.forEachChild(find);
        }
    });
    return result;
}

/*!
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */
/**
 * Finds class property declarations without initializers whose constructor-based initialization
 * can be inlined into the declaration spot after migrating to `inject`. For example:
 *
 * ```ts
 * private foo: number;
 *
 * constructor(private service: MyService) {
 *   this.foo = this.service.getFoo();
 * }
 * ```
 *
 * The initializer of `foo` can be inlined, because `service` will be initialized
 * before it after the `inject` migration has finished running.
 *
 * @param node Class declaration that is being migrated.
 * @param constructor Constructor declaration of the class being migrated.
 * @param localTypeChecker Type checker scoped to the current file.
 */
function findUninitializedPropertiesToCombine(node, constructor, localTypeChecker) {
    let toCombine = null;
    let toHoist = [];
    const membersToDeclarations = new Map();
    for (const member of node.members) {
        if (ts__default["default"].isPropertyDeclaration(member) &&
            !member.initializer &&
            !ts__default["default"].isComputedPropertyName(member.name)) {
            membersToDeclarations.set(member.name.text, member);
        }
    }
    if (membersToDeclarations.size === 0) {
        return null;
    }
    const memberInitializers = getMemberInitializers(constructor);
    if (memberInitializers === null) {
        return null;
    }
    for (const [name, decl] of membersToDeclarations.entries()) {
        if (memberInitializers.has(name)) {
            const initializer = memberInitializers.get(name);
            if (!hasLocalReferences(initializer, constructor, localTypeChecker)) {
                toCombine ??= [];
                toCombine.push({ declaration: membersToDeclarations.get(name), initializer });
            }
        }
        else {
            // Mark members that have no initializers and can't be combined to be hoisted above the
            // injected members. This is either a no-op or it allows us to avoid some patterns internally
            // like the following:
            // ```
            // class Foo {
            //   publicFoo: Foo;
            //   private privateFoo: Foo;
            //
            //   constructor() {
            //     this.initializePrivateFooSomehow();
            //     this.publicFoo = this.privateFoo;
            //   }
            // }
            // ```
            toHoist.push(decl);
        }
    }
    // If no members need to be combined, none need to be hoisted either.
    return toCombine === null ? null : { toCombine, toHoist };
}
/**
 * In some cases properties may be declared out of order, but initialized in the correct order.
 * The internal-specific migration will combine such properties which will result in a compilation
 * error, for example:
 *
 * ```ts
 * class MyClass {
 *   foo: Foo;
 *   bar: Bar;
 *
 *   constructor(bar: Bar) {
 *     this.bar = bar;
 *     this.foo = this.bar.getFoo();
 *   }
 * }
 * ```
 *
 * Will become:
 *
 * ```ts
 * class MyClass {
 *   foo: Foo = this.bar.getFoo();
 *   bar: Bar = inject(Bar);
 * }
 * ```
 *
 * This function determines if cases like this can be saved by reordering the properties so their
 * declaration order matches the order in which they're initialized.
 *
 * @param toCombine Properties that are candidates to be combined.
 * @param constructor
 */
function shouldCombineInInitializationOrder(toCombine, constructor) {
    let combinedMemberReferenceCount = 0;
    let otherMemberReferenceCount = 0;
    const injectedMemberNames = new Set();
    const combinedMemberNames = new Set();
    // Collect the name of constructor parameters that declare new properties.
    // These can be ignored since they'll be hoisted above other properties.
    constructor.parameters.forEach((param) => {
        if (parameterDeclaresProperty(param) && ts__default["default"].isIdentifier(param.name)) {
            injectedMemberNames.add(param.name.text);
        }
    });
    // Collect the names of the properties being combined. We should only reorder
    // the properties if at least one of them refers to another one.
    toCombine.forEach(({ declaration: { name } }) => {
        if (ts__default["default"].isStringLiteralLike(name) || ts__default["default"].isIdentifier(name)) {
            combinedMemberNames.add(name.text);
        }
    });
    // Visit all the initializers and check all the property reads in the form of `this.`.
    // Skip over the ones referring to injected parameters since they're going to be hoisted.
    const walkInitializer = (node) => {
        if (ts__default["default"].isPropertyAccessExpression(node) && node.expression.kind === ts__default["default"].SyntaxKind.ThisKeyword) {
            if (combinedMemberNames.has(node.name.text)) {
                combinedMemberReferenceCount++;
            }
            else if (!injectedMemberNames.has(node.name.text)) {
                otherMemberReferenceCount++;
            }
        }
        node.forEachChild(walkInitializer);
    };
    toCombine.forEach((candidate) => walkInitializer(candidate.initializer));
    // If at the end there is at least one reference between a combined member and another,
    // and there are no references to any other class members, we can safely reorder the
    // properties based on how they were initialized.
    return combinedMemberReferenceCount > 0 && otherMemberReferenceCount === 0;
}
/**
 * Finds the expressions from the constructor that initialize class members, for example:
 *
 * ```ts
 * private foo: number;
 *
 * constructor() {
 *   this.foo = 123;
 * }
 * ```
 *
 * @param constructor Constructor declaration being analyzed.
 */
function getMemberInitializers(constructor) {
    let memberInitializers = null;
    if (!constructor.body) {
        return memberInitializers;
    }
    // Only look at top-level constructor statements.
    for (const node of constructor.body.statements) {
        // Only look for statements in the form of `this. = ;` or `this[] = ;`.
        if (!ts__default["default"].isExpressionStatement(node) ||
            !ts__default["default"].isBinaryExpression(node.expression) ||
            node.expression.operatorToken.kind !== ts__default["default"].SyntaxKind.EqualsToken ||
            (!ts__default["default"].isPropertyAccessExpression(node.expression.left) &&
                !ts__default["default"].isElementAccessExpression(node.expression.left)) ||
            node.expression.left.expression.kind !== ts__default["default"].SyntaxKind.ThisKeyword) {
            continue;
        }
        let name;
        if (ts__default["default"].isPropertyAccessExpression(node.expression.left)) {
            name = node.expression.left.name.text;
        }
        else if (ts__default["default"].isElementAccessExpression(node.expression.left)) {
            name = ts__default["default"].isStringLiteralLike(node.expression.left.argumentExpression)
                ? node.expression.left.argumentExpression.text
                : undefined;
        }
        // If the member is initialized multiple times, take the first one.
        if (name && (!memberInitializers || !memberInitializers.has(name))) {
            memberInitializers = memberInitializers || new Map();
            memberInitializers.set(name, node.expression.right);
        }
    }
    return memberInitializers;
}
/**
 * Determines if a node has references to local symbols defined in the constructor.
 * @param root Expression to check for local references.
 * @param constructor Constructor within which the expression is used.
 * @param localTypeChecker Type checker scoped to the current file.
 */
function hasLocalReferences(root, constructor, localTypeChecker) {
    const sourceFile = root.getSourceFile();
    let hasLocalRefs = false;
    const walk = (node) => {
        // Stop searching if we know that it has local references.
        if (hasLocalRefs) {
            return;
        }
        // Skip identifiers that are accessed via `this` since they're accessing class members
        // that aren't local to the constructor. This is here primarily to catch cases like this
        // where `foo` is defined inside the constructor, but is a class member:
        // ```
        // constructor(private foo: Foo) {
        //   this.bar = this.foo.getFoo();
        // }
        // ```
        if (ts__default["default"].isIdentifier(node) && !isAccessedViaThis(node)) {
            const declarations = localTypeChecker.getSymbolAtLocation(node)?.declarations;
            const isReferencingLocalSymbol = declarations?.some((decl) => 
            // The source file check is a bit redundant since the type checker
            // is local to the file, but it's inexpensive and it can prevent
            // bugs in the future if we decide to use a full type checker.
            decl.getSourceFile() === sourceFile &&
                decl.getStart() >= constructor.getStart() &&
                decl.getEnd() <= constructor.getEnd() &&
                !isInsideInlineFunction(decl, constructor));
            if (isReferencingLocalSymbol) {
                hasLocalRefs = true;
            }
        }
        if (!hasLocalRefs) {
            node.forEachChild(walk);
        }
    };
    walk(root);
    return hasLocalRefs;
}
/**
 * Determines if a node is defined inside of an inline function.
 * @param startNode Node from which to start checking for inline functions.
 * @param boundary Node at which to stop searching.
 */
function isInsideInlineFunction(startNode, boundary) {
    let current = startNode;
    while (current) {
        if (current === boundary) {
            return false;
        }
        if (ts__default["default"].isFunctionDeclaration(current) ||
            ts__default["default"].isFunctionExpression(current) ||
            ts__default["default"].isArrowFunction(current)) {
            return true;
        }
        current = current.parent;
    }
    return false;
}

/**
 * Placeholder used to represent expressions inside the AST.
 * Includes Unicode characters to reduce the chance of collisions.
 */
const PLACEHOLDER = 'ɵɵngGeneratePlaceholderɵɵ';
/**
 * Migrates all of the classes in a `SourceFile` away from constructor injection.
 * @param sourceFile File to be migrated.
 * @param options Options that configure the migration.
 */
function migrateFile(sourceFile, options) {
    // Note: even though externally we have access to the full program with a proper type
    // checker, we create a new one that is local to the file for a couple of reasons:
    // 1. Not having to depend on a program makes running the migration internally faster and easier.
    // 2. All the necessary information for this migration is local so using a file-specific type
    //    checker should speed up the lookups.
    const localTypeChecker = getLocalTypeChecker(sourceFile);
    const analysis = analyzeFile(sourceFile, localTypeChecker, options);
    if (analysis === null || analysis.classes.length === 0) {
        return [];
    }
    const printer = ts__default["default"].createPrinter();
    const tracker = new compiler_host.ChangeTracker(printer);
    analysis.classes.forEach(({ node, constructor, superCall }) => {
        const memberIndentation = leading_space.getLeadingLineWhitespaceOfNode(node.members[0]);
        const prependToClass = [];
        const afterInjectCalls = [];
        const removedStatements = new Set();
        const removedMembers = new Set();
        if (options._internalCombineMemberInitializers) {
            applyInternalOnlyChanges(node, constructor, localTypeChecker, tracker, printer, removedStatements, removedMembers, prependToClass, afterInjectCalls, memberIndentation);
        }
        migrateClass(node, constructor, superCall, options, memberIndentation, prependToClass, afterInjectCalls, removedStatements, removedMembers, localTypeChecker, printer, tracker);
    });
    DI_PARAM_SYMBOLS.forEach((name) => {
        // Both zero and undefined are fine here.
        if (!analysis.nonDecoratorReferences[name]) {
            tracker.removeImport(sourceFile, name, '@angular/core');
        }
    });
    return tracker.recordChanges().get(sourceFile) || [];
}
/**
 * Migrates a class away from constructor injection.
 * @param node Class to be migrated.
 * @param constructor Reference to the class' constructor node.
 * @param superCall Reference to the constructor's `super()` call, if any.
 * @param options Options used to configure the migration.
 * @param memberIndentation Indentation string of the members of the class.
 * @param prependToClass Text that should be prepended to the class.
 * @param afterInjectCalls Text that will be inserted after the newly-added `inject` calls.
 * @param removedStatements Statements that have been removed from the constructor already.
 * @param removedMembers Class members that have been removed by the migration.
 * @param localTypeChecker Type checker set up for the specific file.
 * @param printer Printer used to output AST nodes as strings.
 * @param tracker Object keeping track of the changes made to the file.
 */
function migrateClass(node, constructor, superCall, options, memberIndentation, prependToClass, afterInjectCalls, removedStatements, removedMembers, localTypeChecker, printer, tracker) {
    const sourceFile = node.getSourceFile();
    const unusedParameters = getConstructorUnusedParameters(constructor, localTypeChecker, removedStatements);
    const superParameters = superCall
        ? getSuperParameters(constructor, superCall, localTypeChecker)
        : null;
    const removedStatementCount = removedStatements.size;
    const firstConstructorStatement = constructor.body?.statements.find((statement) => !removedStatements.has(statement));
    const innerReference = superCall || firstConstructorStatement || constructor;
    const innerIndentation = leading_space.getLeadingLineWhitespaceOfNode(innerReference);
    const prependToConstructor = [];
    const afterSuper = [];
    for (const param of constructor.parameters) {
        const usedInSuper = superParameters !== null && superParameters.has(param);
        const usedInConstructor = !unusedParameters.has(param);
        const usesOtherParams = parameterReferencesOtherParameters(param, constructor.parameters, localTypeChecker);
        migrateParameter(param, options, localTypeChecker, printer, tracker, superCall, usedInSuper, usedInConstructor, usesOtherParams, memberIndentation, innerIndentation, prependToConstructor, prependToClass, afterSuper);
    }
    // Delete all of the constructor overloads since below we're either going to
    // remove the implementation, or we're going to delete all of the parameters.
    for (const member of node.members) {
        if (ts__default["default"].isConstructorDeclaration(member) && member !== constructor) {
            removedMembers.add(member);
            tracker.removeNode(member, true);
        }
    }
    if (canRemoveConstructor(options, constructor, removedStatementCount, prependToConstructor, superCall)) {
        // Drop the constructor if it was empty.
        removedMembers.add(constructor);
        tracker.removeNode(constructor, true);
    }
    else {
        // If the constructor contains any statements, only remove the parameters.
        // We always do this no matter what is passed into `backwardsCompatibleConstructors`.
        stripConstructorParameters(constructor, tracker);
        if (prependToConstructor.length > 0) {
            if (firstConstructorStatement ||
                (innerReference !== constructor &&
                    innerReference.getStart() >= constructor.getStart() &&
                    innerReference.getEnd() <= constructor.getEnd())) {
                tracker.insertText(sourceFile, (firstConstructorStatement || innerReference).getFullStart(), `\n${prependToConstructor.join('\n')}\n`);
            }
            else {
                tracker.insertText(sourceFile, constructor.body.getStart() + 1, `\n${prependToConstructor.map((p) => innerIndentation + p).join('\n')}\n${innerIndentation}`);
            }
        }
    }
    if (afterSuper.length > 0 && superCall !== null) {
        // Note that if we can, we should insert before the next statement after the `super` call,
        // rather than after the end of it. Otherwise the string buffering implementation may drop
        // the text if the statement after the `super` call is being deleted. This appears to be because
        // the full start of the next statement appears to always be the end of the `super` call plus 1.
        const nextStatement = getNextPreservedStatement(superCall, removedStatements);
        tracker.insertText(sourceFile, nextStatement ? nextStatement.getFullStart() : constructor.getEnd() - 1, `\n${afterSuper.join('\n')}\n` + (nextStatement ? '' : memberIndentation));
    }
    // Need to resolve this once all constructor signatures have been removed.
    const memberReference = node.members.find((m) => !removedMembers.has(m)) || node.members[0];
    // If `backwardsCompatibleConstructors` is enabled, we maintain
    // backwards compatibility by adding a catch-all signature.
    if (options.backwardsCompatibleConstructors) {
        const extraSignature = `\n${memberIndentation}/** Inserted by Angular inject() migration for backwards compatibility */\n` +
            `${memberIndentation}constructor(...args: unknown[]);`;
        // The new signature always has to be right before the constructor implementation.
        if (memberReference === constructor) {
            prependToClass.push(extraSignature);
        }
        else {
            tracker.insertText(sourceFile, constructor.getFullStart(), '\n' + extraSignature);
        }
    }
    // Push the block of code that should appear after the `inject`
    // calls now once all the members have been generated.
    prependToClass.push(...afterInjectCalls);
    if (prependToClass.length > 0) {
        if (removedMembers.size === node.members.length) {
            tracker.insertText(sourceFile, 
            // If all members were deleted, insert after the last one.
            // This allows us to preserve the indentation.
            node.members.length > 0
                ? node.members[node.members.length - 1].getEnd() + 1
                : node.getEnd() - 1, `${prependToClass.join('\n')}\n`);
        }
        else {
            // Insert the new properties after the first member that hasn't been deleted.
            tracker.insertText(sourceFile, memberReference.getFullStart(), `\n${prependToClass.join('\n')}\n`);
        }
    }
}
/**
 * Migrates a single parameter to `inject()` DI.
 * @param node Parameter to be migrated.
 * @param options Options used to configure the migration.
 * @param localTypeChecker Type checker set up for the specific file.
 * @param printer Printer used to output AST nodes as strings.
 * @param tracker Object keeping track of the changes made to the file.
 * @param superCall Call to `super()` from the class' constructor.
 * @param usedInSuper Whether the parameter is referenced inside of `super`.
 * @param usedInConstructor Whether the parameter is referenced inside the body of the constructor.
 * @param memberIndentation Indentation string to use when inserting new class members.
 * @param innerIndentation Indentation string to use when inserting new constructor statements.
 * @param prependToConstructor Statements to be prepended to the constructor.
 * @param propsToAdd Properties to be added to the class.
 * @param afterSuper Statements to be added after the `super` call.
 */
function migrateParameter(node, options, localTypeChecker, printer, tracker, superCall, usedInSuper, usedInConstructor, usesOtherParams, memberIndentation, innerIndentation, prependToConstructor, propsToAdd, afterSuper) {
    if (!ts__default["default"].isIdentifier(node.name)) {
        return;
    }
    const name = node.name.text;
    const replacementCall = createInjectReplacementCall(node, options, localTypeChecker, printer, tracker);
    const declaresProp = parameterDeclaresProperty(node);
    // If the parameter declares a property, we need to declare it (e.g. `private foo: Foo`).
    if (declaresProp) {
        // We can't initialize the property if it's referenced within a `super` call or  it references
        // other parameters. See the logic further below for the initialization.
        const canInitialize = !usedInSuper && !usesOtherParams;
        const prop = ts__default["default"].factory.createPropertyDeclaration(cloneModifiers(node.modifiers?.filter((modifier) => {
            // Strip out the DI decorators, as well as `public` which is redundant.
            return !ts__default["default"].isDecorator(modifier) && modifier.kind !== ts__default["default"].SyntaxKind.PublicKeyword;
        })), name, 
        // Don't add the question token to private properties since it won't affect interface implementation.
        node.modifiers?.some((modifier) => modifier.kind === ts__default["default"].SyntaxKind.PrivateKeyword)
            ? undefined
            : node.questionToken, canInitialize ? undefined : node.type, canInitialize ? ts__default["default"].factory.createIdentifier(PLACEHOLDER) : undefined);
        propsToAdd.push(memberIndentation +
            replaceNodePlaceholder(node.getSourceFile(), prop, replacementCall, printer));
    }
    // If the parameter is referenced within the constructor, we need to declare it as a variable.
    if (usedInConstructor) {
        if (usedInSuper) {
            // Usages of `this` aren't allowed before `super` calls so we need to
            // create a variable which calls `inject()` directly instead...
            prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
            // ...then we can initialize the property after the `super` call.
            if (declaresProp) {
                afterSuper.push(`${innerIndentation}this.${name} = ${name};`);
            }
        }
        else if (declaresProp) {
            // If the parameter declares a property (`private foo: foo`) and is used inside the class
            // at the same time, we need to ensure that it's initialized to the value from the variable
            // and that we only reference `this` after the `super` call.
            const initializer = `${innerIndentation}const ${name} = this.${name};`;
            if (superCall === null) {
                prependToConstructor.push(initializer);
            }
            else {
                afterSuper.push(initializer);
            }
        }
        else {
            // If the parameter is only referenced in the constructor, we
            // don't need to declare any new properties.
            prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
        }
    }
    else if (usesOtherParams && declaresProp) {
        const toAdd = `${innerIndentation}this.${name} = ${replacementCall};`;
        if (superCall === null) {
            prependToConstructor.push(toAdd);
        }
        else {
            afterSuper.push(toAdd);
        }
    }
}
/**
 * Creates a replacement `inject` call from a function parameter.
 * @param param Parameter for which to generate the `inject` call.
 * @param options Options used to configure the migration.
 * @param localTypeChecker Type checker set up for the specific file.
 * @param printer Printer used to output AST nodes as strings.
 * @param tracker Object keeping track of the changes made to the file.
 */
function createInjectReplacementCall(param, options, localTypeChecker, printer, tracker) {
    const moduleName = '@angular/core';
    const sourceFile = param.getSourceFile();
    const decorators = nodes.getAngularDecorators(localTypeChecker, ts__default["default"].getDecorators(param) || []);
    const literalProps = [];
    const type = param.type;
    let injectedType = '';
    let typeArguments = type && hasGenerics(type) ? [type] : undefined;
    let hasOptionalDecorator = false;
    if (type) {
        // Remove the type arguments from generic type references, because
        // they'll be specified as type arguments to `inject()`.
        if (ts__default["default"].isTypeReferenceNode(type) && type.typeArguments && type.typeArguments.length > 0) {
            injectedType = type.typeName.getText();
        }
        else if (ts__default["default"].isUnionTypeNode(type)) {
            injectedType = (type.types.find((t) => !ts__default["default"].isLiteralTypeNode(t)) || type.types[0]).getText();
        }
        else {
            injectedType = type.getText();
        }
    }
    for (const decorator of decorators) {
        if (decorator.moduleName !== moduleName) {
            continue;
        }
        const firstArg = decorator.node.expression.arguments[0];
        switch (decorator.name) {
            case 'Inject':
                if (firstArg) {
                    const injectResult = migrateInjectDecorator(firstArg, type, localTypeChecker);
                    injectedType = injectResult.injectedType;
                    if (injectResult.typeArguments) {
                        typeArguments = injectResult.typeArguments;
                    }
                }
                break;
            case 'Attribute':
                if (firstArg) {
                    const constructorRef = tracker.addImport(sourceFile, 'HostAttributeToken', moduleName);
                    const expression = ts__default["default"].factory.createNewExpression(constructorRef, undefined, [firstArg]);
                    injectedType = printer.printNode(ts__default["default"].EmitHint.Unspecified, expression, sourceFile);
                    typeArguments = undefined;
                }
                break;
            case 'Optional':
                hasOptionalDecorator = true;
                literalProps.push(ts__default["default"].factory.createPropertyAssignment('optional', ts__default["default"].factory.createTrue()));
                break;
            case 'SkipSelf':
                literalProps.push(ts__default["default"].factory.createPropertyAssignment('skipSelf', ts__default["default"].factory.createTrue()));
                break;
            case 'Self':
                literalProps.push(ts__default["default"].factory.createPropertyAssignment('self', ts__default["default"].factory.createTrue()));
                break;
            case 'Host':
                literalProps.push(ts__default["default"].factory.createPropertyAssignment('host', ts__default["default"].factory.createTrue()));
                break;
        }
    }
    // The injected type might be a `TypeNode` which we can't easily convert into an `Expression`.
    // Since the value gets passed through directly anyway, we generate the call using a placeholder
    // which we then replace with the raw text of the `TypeNode`.
    const injectRef = tracker.addImport(param.getSourceFile(), 'inject', moduleName);
    const args = [ts__default["default"].factory.createIdentifier(PLACEHOLDER)];
    if (literalProps.length > 0) {
        args.push(ts__default["default"].factory.createObjectLiteralExpression(literalProps));
    }
    let expression = ts__default["default"].factory.createCallExpression(injectRef, typeArguments, args);
    if (hasOptionalDecorator && options.nonNullableOptional) {
        const hasNullableType = param.questionToken != null || (param.type != null && isNullableType(param.type));
        // Only wrap the expression if the type wasn't already nullable.
        // If it was, the app was likely accounting for it already.
        if (!hasNullableType) {
            expression = ts__default["default"].factory.createNonNullExpression(expression);
        }
    }
    // If the parameter is initialized, add the initializer as a fallback.
    if (param.initializer) {
        expression = ts__default["default"].factory.createBinaryExpression(expression, ts__default["default"].SyntaxKind.QuestionQuestionToken, param.initializer);
    }
    return replaceNodePlaceholder(param.getSourceFile(), expression, injectedType, printer);
}
/**
 * Migrates a parameter based on its `@Inject()` decorator.
 * @param firstArg First argument to `@Inject()`.
 * @param type Type of the parameter.
 * @param localTypeChecker Type checker set up for the specific file.
 */
function migrateInjectDecorator(firstArg, type, localTypeChecker) {
    let injectedType = firstArg.getText();
    let typeArguments = null;
    // `inject` no longer officially supports string injection so we need
    // to cast to any. We maintain the type by passing it as a generic.
    if (ts__default["default"].isStringLiteralLike(firstArg)) {
        typeArguments = [type || ts__default["default"].factory.createKeywordTypeNode(ts__default["default"].SyntaxKind.AnyKeyword)];
        injectedType += ' as any';
    }
    else if (ts__default["default"].isCallExpression(firstArg) &&
        ts__default["default"].isIdentifier(firstArg.expression) &&
        firstArg.arguments.length === 1) {
        const callImport = imports.getImportOfIdentifier(localTypeChecker, firstArg.expression);
        const arrowFn = firstArg.arguments[0];
        // If the first parameter is a `forwardRef`, unwrap it for a more
        // accurate type and because it's no longer necessary.
        if (callImport !== null &&
            callImport.name === 'forwardRef' &&
            callImport.importModule === '@angular/core' &&
            ts__default["default"].isArrowFunction(arrowFn)) {
            if (ts__default["default"].isBlock(arrowFn.body)) {
                const returnStatement = arrowFn.body.statements.find((stmt) => ts__default["default"].isReturnStatement(stmt));
                if (returnStatement && returnStatement.expression) {
                    injectedType = returnStatement.expression.getText();
                }
            }
            else {
                injectedType = arrowFn.body.getText();
            }
        }
    }
    else if (type &&
        (ts__default["default"].isTypeReferenceNode(type) ||
            ts__default["default"].isTypeLiteralNode(type) ||
            ts__default["default"].isTupleTypeNode(type) ||
            (ts__default["default"].isUnionTypeNode(type) && type.types.some(ts__default["default"].isTypeReferenceNode)))) {
        typeArguments = [type];
    }
    return { injectedType, typeArguments };
}
/**
 * Removes the parameters from a constructor. This is a bit more complex than just replacing an AST
 * node, because `NodeArray.pos` includes any leading whitespace, but `NodeArray.end` does **not**
 * include trailing whitespace. Since we want to produce somewhat formatted code, we need to find
 * the end of the arguments ourselves. We do it by finding the next parenthesis after the last
 * parameter.
 * @param node Constructor from which to remove the parameters.
 * @param tracker Object keeping track of the changes made to the file.
 */
function stripConstructorParameters(node, tracker) {
    if (node.parameters.length === 0) {
        return;
    }
    const constructorText = node.getText();
    const lastParamText = node.parameters[node.parameters.length - 1].getText();
    const lastParamStart = constructorText.indexOf(lastParamText);
    // This shouldn't happen, but bail out just in case so we don't mangle the code.
    if (lastParamStart === -1) {
        return;
    }
    for (let i = lastParamStart + lastParamText.length; i < constructorText.length; i++) {
        const char = constructorText[i];
        if (char === ')') {
            tracker.replaceText(node.getSourceFile(), node.parameters.pos, node.getStart() + i - node.parameters.pos, '');
            break;
        }
    }
}
/**
 * Creates a type checker scoped to a specific file.
 * @param sourceFile File for which to create the type checker.
 */
function getLocalTypeChecker(sourceFile) {
    const options = { noEmit: true, skipLibCheck: true };
    const host = ts__default["default"].createCompilerHost(options);
    host.getSourceFile = (fileName) => (fileName === sourceFile.fileName ? sourceFile : undefined);
    const program = ts__default["default"].createProgram({
        rootNames: [sourceFile.fileName],
        options,
        host,
    });
    return program.getTypeChecker();
}
/**
 * Prints out an AST node and replaces the placeholder inside of it.
 * @param sourceFile File in which the node will be inserted.
 * @param node Node to be printed out.
 * @param replacement Replacement for the placeholder.
 * @param printer Printer used to output AST nodes as strings.
 */
function replaceNodePlaceholder(sourceFile, node, replacement, printer) {
    const result = printer.printNode(ts__default["default"].EmitHint.Unspecified, node, sourceFile);
    return result.replace(PLACEHOLDER, replacement);
}
/**
 * Clones an optional array of modifiers. Can be useful to
 * strip the comments from a node with modifiers.
 */
function cloneModifiers(modifiers) {
    return modifiers?.map((modifier) => {
        return ts__default["default"].isDecorator(modifier)
            ? ts__default["default"].factory.createDecorator(modifier.expression)
            : ts__default["default"].factory.createModifier(modifier.kind);
    });
}
/**
 * Clones the name of a property. Can be useful to strip away
 * the comments of a property without modifiers.
 */
function cloneName(node) {
    switch (node.kind) {
        case ts__default["default"].SyntaxKind.Identifier:
            return ts__default["default"].factory.createIdentifier(node.text);
        case ts__default["default"].SyntaxKind.StringLiteral:
            return ts__default["default"].factory.createStringLiteral(node.text, node.getText()[0] === `'`);
        case ts__default["default"].SyntaxKind.NoSubstitutionTemplateLiteral:
            return ts__default["default"].factory.createNoSubstitutionTemplateLiteral(node.text, node.rawText);
        case ts__default["default"].SyntaxKind.NumericLiteral:
            return ts__default["default"].factory.createNumericLiteral(node.text);
        case ts__default["default"].SyntaxKind.ComputedPropertyName:
            return ts__default["default"].factory.createComputedPropertyName(node.expression);
        case ts__default["default"].SyntaxKind.PrivateIdentifier:
            return ts__default["default"].factory.createPrivateIdentifier(node.text);
        default:
            return node;
    }
}
/**
 * Determines whether it's safe to delete a class constructor.
 * @param options Options used to configure the migration.
 * @param constructor Node representing the constructor.
 * @param removedStatementCount Number of statements that were removed by the migration.
 * @param prependToConstructor Statements that should be prepended to the constructor.
 * @param superCall Node representing the `super()` call within the constructor.
 */
function canRemoveConstructor(options, constructor, removedStatementCount, prependToConstructor, superCall) {
    if (options.backwardsCompatibleConstructors || prependToConstructor.length > 0) {
        return false;
    }
    const statementCount = constructor.body
        ? constructor.body.statements.length - removedStatementCount
        : 0;
    return (statementCount === 0 ||
        (statementCount === 1 && superCall !== null && superCall.arguments.length === 0));
}
/**
 * Gets the next statement after a node that *won't* be deleted by the migration.
 * @param startNode Node from which to start the search.
 * @param removedStatements Statements that have been removed by the migration.
 * @returns
 */
function getNextPreservedStatement(startNode, removedStatements) {
    const body = nodes.closestNode(startNode, ts__default["default"].isBlock);
    const closestStatement = nodes.closestNode(startNode, ts__default["default"].isStatement);
    if (body === null || closestStatement === null) {
        return null;
    }
    const index = body.statements.indexOf(closestStatement);
    if (index === -1) {
        return null;
    }
    for (let i = index + 1; i < body.statements.length; i++) {
        if (!removedStatements.has(body.statements[i])) {
            return body.statements[i];
        }
    }
    return null;
}
/**
 * Applies the internal-specific migrations to a class.
 * @param node Class being migrated.
 * @param constructor The migrated class' constructor.
 * @param localTypeChecker File-specific type checker.
 * @param tracker Object keeping track of the changes.
 * @param printer Printer used to output AST nodes as text.
 * @param removedStatements Statements that have been removed by the migration.
 * @param removedMembers Class members that have been removed by the migration.
 * @param prependToClass Text that will be prepended to a class.
 * @param afterInjectCalls Text that will be inserted after the newly-added `inject` calls.
 * @param memberIndentation Indentation string of the class' members.
 */
function applyInternalOnlyChanges(node, constructor, localTypeChecker, tracker, printer, removedStatements, removedMembers, prependToClass, afterInjectCalls, memberIndentation) {
    const result = findUninitializedPropertiesToCombine(node, constructor, localTypeChecker);
    if (result === null) {
        return;
    }
    const preserveInitOrder = shouldCombineInInitializationOrder(result.toCombine, constructor);
    // Sort the combined members based on the declaration order of their initializers, only if
    // we've determined that would be safe. Note that `Array.prototype.sort` is in-place so we
    // can just call it conditionally here.
    if (preserveInitOrder) {
        result.toCombine.sort((a, b) => a.initializer.getStart() - b.initializer.getStart());
    }
    result.toCombine.forEach(({ declaration, initializer }) => {
        const initializerStatement = nodes.closestNode(initializer, ts__default["default"].isStatement);
        const newProperty = ts__default["default"].factory.createPropertyDeclaration(cloneModifiers(declaration.modifiers), cloneName(declaration.name), declaration.questionToken, declaration.type, initializer);
        // If the initialization order is being preserved, we have to remove the original
        // declaration and re-declare it. Otherwise we can do the replacement in-place.
        if (preserveInitOrder) {
            tracker.removeNode(declaration, true);
            removedMembers.add(declaration);
            afterInjectCalls.push(memberIndentation +
                printer.printNode(ts__default["default"].EmitHint.Unspecified, newProperty, declaration.getSourceFile()));
        }
        else {
            tracker.replaceNode(declaration, newProperty);
        }
        // This should always be defined, but null check it just in case.
        if (initializerStatement) {
            tracker.removeNode(initializerStatement, true);
            removedStatements.add(initializerStatement);
        }
    });
    result.toHoist.forEach((decl) => {
        prependToClass.push(memberIndentation + printer.printNode(ts__default["default"].EmitHint.Unspecified, decl, decl.getSourceFile()));
        tracker.removeNode(decl, true);
        removedMembers.add(decl);
    });
    // If we added any hoisted properties, separate them visually with a new line.
    if (prependToClass.length > 0) {
        prependToClass.push('');
    }
}

function migrate(options) {
    return async (tree) => {
        const basePath = process.cwd();
        const pathToMigrate = compiler_host.normalizePath(p.join(basePath, options.path));
        let allPaths = [];
        if (pathToMigrate.trim() !== '') {
            allPaths.push(pathToMigrate);
        }
        if (!allPaths.length) {
            throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the inject migration.');
        }
        for (const tsconfigPath of allPaths) {
            runInjectMigration(tree, tsconfigPath, basePath, pathToMigrate, options);
        }
    };
}
function runInjectMigration(tree, tsconfigPath, basePath, pathToMigrate, schematicOptions) {
    if (schematicOptions.path.startsWith('..')) {
        throw new schematics.SchematicsException('Cannot run inject migration outside of the current project.');
    }
    const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
    const sourceFiles = program
        .getSourceFiles()
        .filter((sourceFile) => sourceFile.fileName.startsWith(pathToMigrate) &&
        compiler_host.canMigrateFile(basePath, sourceFile, program));
    if (sourceFiles.length === 0) {
        throw new schematics.SchematicsException(`Could not find any files to migrate under the path ${pathToMigrate}. Cannot run the inject migration.`);
    }
    for (const sourceFile of sourceFiles) {
        const changes = migrateFile(sourceFile, schematicOptions);
        if (changes.length > 0) {
            const update = tree.beginUpdate(p.relative(basePath, sourceFile.fileName));
            for (const change of changes) {
                if (change.removeLength != null) {
                    update.remove(change.start, change.removeLength);
                }
                update.insertRight(change.start, change.text);
            }
            tree.commitUpdate(update);
        }
    }
}

exports.migrate = migrate;




© 2015 - 2025 Weber Informatics LLC | Privacy Policy