Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 20 additions & 119 deletions rewrite-javascript/rewrite/src/javascript/add-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {randomId} from "../uuid";
import {emptyMarkers, markers} from "../markers";
import {getStyle, PrettierStyle, SpacesStyle, StyleKind} from "./style";
import {Cursor} from "../tree";
import {bindingNames, namesDeclaredIn} from "./scope";

export type QuoteChar = "'" | '"';

Expand Down Expand Up @@ -143,19 +144,18 @@ export function maybeAddImport(
}

const derived = derivedName(options);
const cu = compilationUnitOf(visitor);
const cursor = cursorOf(visitor);
const cu = cursor && compilationUnitOf(cursor);
if (!cu) {
visitor.afterVisit.push(new AddImport(options, derived));
return derived;
}

const bindings = bindingsInScope(cu, cursorOf(visitor));

// An import already serving this request answers it; queuing one would, on the next cycle,
// derive a suffixed name from the binding this call just added. A caller that named a preference
// takes whatever comes back; one that did not assumes the name it derived, so a binding under
// any other name would leave the references it emits unbound.
for (const binding of bindings) {
for (const binding of moduleScopeBindings(cu)) {
if (binding.module === module && binding.member === memberName(options.member) &&
binding.typeOnly === typeOnly &&
(options.preferredName !== undefined || anyNameAnswers(options) ||
Expand All @@ -164,7 +164,10 @@ export function maybeAddImport(
}
}

const name = deconflict(derived, takenNames(bindings, visitor));
// Only the module scope answers for a name, but any scope in the file occupies one. The queue
// gives every later request for this module the name chosen here, so it has to clear the scopes
// those references will sit in, which are not known yet.
const name = deconflict(derived, takenNames(namesDeclaredIn(cu), visitor));
visitor.afterVisit.push(new AddImport(options, name));
return name;
}
Expand Down Expand Up @@ -230,73 +233,29 @@ interface ModuleScopeBinding {
}

function cursorOf(visitor: JavaScriptVisitor<any>): Cursor | undefined {
return (visitor as unknown as { cursor?: Cursor }).cursor;
}

function compilationUnitOf(visitor: JavaScriptVisitor<any>): JS.CompilationUnit | undefined {
// `cursor` is protected on `TreeVisitor`, and the `maybeAddImport`/`maybeRemoveImport`
// API is free functions, so reaching it takes a cast.
return cursorOf(visitor)?.firstEnclosing((v): v is JS.CompilationUnit => v?.kind === JS.Kind.CompilationUnit);
return (visitor as unknown as { cursor?: Cursor }).cursor;
}

/** Every name in scope at `cursor`: what the file binds, and what each block enclosing it declares. */
function bindingsInScope(cu: JS.CompilationUnit, cursor: Cursor | undefined): ModuleScopeBinding[] {
const bindings = statementBindings(cu.statements);
// A local shadows an import for the code the template lands in, so a name a block declares is
// taken there even though the module never answers for it.
for (let c = cursor; c && c.value !== cu; c = c.parent) {
for (const name of scopeNames(c.value)) {
bindings.push({name});
}
}
return bindings;
function compilationUnitOf(cursor: Cursor): JS.CompilationUnit | undefined {
return cursor.firstEnclosing((v): v is JS.CompilationUnit => v?.kind === JS.Kind.CompilationUnit);
}

/** The names a scope introduces directly: a block's declarations, or a function's parameters. */
function scopeNames(scope: unknown): string[] {
const node = scope as J | undefined;
if (node?.kind === J.Kind.Block) {
return statementBindings((node as J.Block).statements).map(binding => binding.name);
}
if (node?.kind === J.Kind.MethodDeclaration) {
return (node as J.MethodDeclaration).parameters.elements
.flatMap(param => param.element?.kind === J.Kind.VariableDeclarations
? (param.element as J.VariableDeclarations).variables.flatMap(v => patternNames(v.element?.name))
: patternNames(param.element))
.map(bound => bound.name);
}
if (node?.kind === J.Kind.Lambda) {
return (node as J.Lambda).parameters.parameters
.flatMap(param => param.element?.kind === J.Kind.VariableDeclarations
? (param.element as J.VariableDeclarations).variables.flatMap(v => patternNames(v.element?.name))
: patternNames(param.element))
.map(bound => bound.name);
}
return [];
}

/** Imports are top-level statements and so is anything that can shadow one, so a flat scan sees every name. */
function statementBindings(statements: J.RightPadded<Statement>[]): ModuleScopeBinding[] {
/** What the file's imports and `require`s bind at module scope, and the module each name comes from. */
function moduleScopeBindings(cu: JS.CompilationUnit): ModuleScopeBinding[] {
const bindings: ModuleScopeBinding[] = [];

const declaredBy = (name: J | undefined): void => {
for (const bound of patternNames(name)) {
bindings.push({name: bound.name});
}
};

const declaredByVariables = (varDecl: J.VariableDeclarations): void => {
for (const variable of varDecl.variables) {
const required = requiredModule(variable.element?.initializer?.element);
if (required !== undefined) {
bindings.push(...requireBindings(variable.element?.name, required));
} else {
declaredBy(variable.element?.name);
}
}
};

for (const stmt of statements) {
for (const stmt of cu.statements) {
const statement = stmt.element;
switch (statement?.kind) {
case JS.Kind.Import:
Expand All @@ -312,18 +271,6 @@ function statementBindings(statements: J.RightPadded<Statement>[]): ModuleScopeB
}
}
break;
case J.Kind.MethodDeclaration:
declaredBy((statement as J.MethodDeclaration).name);
break;
case J.Kind.ClassDeclaration:
declaredBy((statement as J.ClassDeclaration).name);
break;
case JS.Kind.NamespaceDeclaration:
declaredBy((statement as JS.NamespaceDeclaration).name.element);
break;
case JS.Kind.TypeDeclaration:
declaredBy((statement as JS.TypeDeclaration).name.element);
break;
}
}

Expand Down Expand Up @@ -357,55 +304,13 @@ function requiredModuleOf(methodInv: J.MethodInvocation): string | undefined {
: undefined;
}

/**
* Every name a binding pattern introduces. `member` is the property a name takes its value from,
* and only a name bound directly by the pattern has one — anything deeper reads a property of a
* property, so it occupies its name without binding a member of the module.
*/
function patternNames(pattern: J | undefined): { name: string; member?: string }[] {
if (pattern?.kind === J.Kind.Identifier) {
return [{name: (pattern as J.Identifier).simpleName}];
}
// An array pattern binds by position, so its elements name no member of what they destructure.
if (pattern?.kind === JS.Kind.ArrayBindingPattern) {
return (pattern as JS.ArrayBindingPattern).elements.elements
.flatMap(elem => elem.element?.kind === JS.Kind.BindingElement
? patternNames((elem.element as JS.BindingElement).name)
: patternNames(elem.element))
.map(bound => ({name: bound.name}));
}
if (pattern?.kind !== JS.Kind.ObjectBindingPattern) {
return [];
}
const names: { name: string; member?: string }[] = [];
for (const elem of (pattern as JS.ObjectBindingPattern).bindings.elements) {
if (elem.element?.kind !== JS.Kind.BindingElement) {
continue;
}
const bindingElem = elem.element as JS.BindingElement;
if (bindingElem.name?.kind === J.Kind.Identifier) {
const name = (bindingElem.name as J.Identifier).simpleName;
const propertyName = bindingElem.propertyName?.element;
names.push({
name,
member: propertyName?.kind === J.Kind.Identifier
? (propertyName as J.Identifier).simpleName
: name
});
} else {
names.push(...patternNames(bindingElem.name).map(bound => ({name: bound.name})));
}
}
return names;
}

/** A `require` binds a module the way an import does, so the pool records it the same way. */
function requireBindings(pattern: J | undefined, module: string): ModuleScopeBinding[] {
// A whole-module require binds no member, exactly as a default import does.
if (pattern?.kind === J.Kind.Identifier) {
return [{name: (pattern as J.Identifier).simpleName, module, member: undefined, typeOnly: false}];
}
return patternNames(pattern).map(bound => bound.member === undefined
return bindingNames(pattern).map(bound => bound.member === undefined
? {name: bound.name}
: {name: bound.name, module, member: bound.member, typeOnly: false});
}
Expand Down Expand Up @@ -465,23 +370,19 @@ function importBindings(jsImport: JS.Import): ModuleScopeBinding[] {
}

/**
* Names the file binds, plus those pending `AddImport`s on the `afterVisit` queue have claimed. A
* queued `RemoveImport` does not free one: it removes only what the file leaves unused, and binding
* a name it keeps is an error, where an unnecessary suffix merely reads oddly.
* Names in scope, plus those pending `AddImport`s on the `afterVisit` queue have claimed. A queued
* `RemoveImport` does not free one: it removes only what the file leaves unused, and binding a name
* it keeps is an error, where an unnecessary suffix merely reads oddly.
*/
function takenNames(bindings: ModuleScopeBinding[], visitor: JavaScriptVisitor<any>): Set<string> {
const taken = new Set<string>();
function takenNames(inScope: ReadonlySet<string>, visitor: JavaScriptVisitor<any>): Set<string> {
const taken = new Set<string>(inScope);

for (const v of visitor.afterVisit || []) {
if (v instanceof AddImport && v.bindingName) {
taken.add(v.bindingName);
}
}

for (const binding of bindings) {
taken.add(binding.name);
}

return taken;
}

Expand Down
1 change: 1 addition & 0 deletions rewrite-javascript/rewrite/src/javascript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export * from "./autodetect";
export * from "./tree-debug";
export * from "./project-parser";

export * from "./scope";
export * from "./add-import";
export * from "./remove-import";
export * from "./cleanup/index";
Expand Down
41 changes: 20 additions & 21 deletions rewrite-javascript/rewrite/src/javascript/remove-import.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {JavaScriptVisitor} from "./visitor";
import {J} from "../java";
import {bindingNames} from "./scope";
import {JS, JSX} from "./tree";
import {mapAsync, updateIfChanged} from "../util";
import {ElementRemovalFormatter} from "../java";
Expand Down Expand Up @@ -268,7 +269,7 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
shouldRemove = !usedIdentifiers.has(name) && !usedTypes.has(name);
} else {
// Regular case: check if the import name matches the removal criteria
shouldRemove = this.shouldRemoveImport(name, usedIdentifiers, usedTypes);
shouldRemove = this.shouldRemoveImport(name, usedIdentifiers, usedTypes, name);
}

if (shouldRemove) {
Expand Down Expand Up @@ -325,7 +326,7 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
}, p);
}
// Namespace is used, we can't remove individual members from it
} else if (this.shouldRemoveImport(name, usedIdentifiers, usedTypes)) {
} else if (this.shouldRemoveImport(name, usedIdentifiers, usedTypes, name)) {
// If there's no default import, remove the entire import
if (!importClause.name) {
return undefined;
Expand Down Expand Up @@ -355,7 +356,7 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
}, p);
}
// Namespace is used, we can't remove individual members from it
} else if (this.shouldRemoveImport(aliasName, usedIdentifiers, usedTypes)) {
} else if (this.shouldRemoveImport(aliasName, usedIdentifiers, usedTypes, aliasName)) {
// If there's no default import, remove the entire import
if (!importClause.name) {
return undefined;
Expand Down Expand Up @@ -490,7 +491,7 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
return true; // Keep imports that don't match the member
} else {
// We're removing based on the import name itself
return !this.shouldRemoveImport(importName, usedIdentifiers, usedTypes);
return !this.shouldRemoveImport(importName, usedIdentifiers, usedTypes, importName);
}
}
return true; // Keep non-ImportSpecifier elements
Expand Down Expand Up @@ -612,14 +613,10 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
const {filtered, allRemoved} = await this.filterElementsWithPrefixPreservation(
pattern.bindings.elements,
(elem: J) => {
if (elem.kind === JS.Kind.BindingElement) {
const name = this.getBindingElementName(elem as JS.BindingElement);
return !this.shouldRemoveImport(name, usedIdentifiers, new Set());
} else if (elem.kind === J.Kind.Identifier) {
const name = (elem as J.Identifier).simpleName;
return !this.shouldRemoveImport(name, usedIdentifiers, new Set());
}
return true; // Keep other element types
const bound = this.boundByPatternElement(elem);
// An element whose names cannot be read is left alone.
return bound.length === 0 ||
bound.some(b => !this.shouldRemoveImport(b.name, usedIdentifiers, new Set(), b.member));
},
async (elem: J, prefix: J.Space) => {
if (elem.kind === J.Kind.Identifier) {
Expand Down Expand Up @@ -687,23 +684,25 @@ export class RemoveImport<P> extends JavaScriptVisitor<P> {
return undefined;
}

private getBindingElementName(bindingElement: JS.BindingElement): string {
const name = bindingElement.name;
if (name?.kind === J.Kind.Identifier) {
return (name as J.Identifier).simpleName;
}
return '';
/** The names a pattern element binds, and for each the member of the module it reads. */
private boundByPatternElement(elem: J): { name: string; member?: string }[] {
// Shorthand, so the name it binds is the member it reads.
return elem.kind === J.Kind.Identifier
? [{name: (elem as J.Identifier).simpleName, member: (elem as J.Identifier).simpleName}]
: bindingNames(elem);
}

private shouldRemoveImport(
name: string,
usedIdentifiers: Set<string>,
usedTypes: Set<string>
usedTypes: Set<string>,
member: string | undefined
): boolean {
// If member is specified, we're removing a specific member from the module
if (this.member !== undefined) {
// Only remove if this is the specific member we're looking for
if (this.member !== name) {
// A name bound under an alias reads one member and is referenced by another, and a name
// a nested pattern binds reads a property of a property, so it reads no member at all.
if (this.member !== member) {
return false;
}
}
Expand Down
Loading