Skip to content

Commit

Permalink
Analyze exports and add ability to dereference References to them
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinpschaaf committed Oct 19, 2022
1 parent de77109 commit 1d37f5d
Show file tree
Hide file tree
Showing 5 changed files with 462 additions and 100 deletions.
2 changes: 2 additions & 0 deletions packages/labs/analyzer/src/lib/javascript/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
isLitElement,
getLitElementDeclaration,
} from '../lit-element/lit-element.js';
import {isExport} from '../references.js';

/**
* Returns an analyzer `ClassDeclaration` model for the given
Expand Down Expand Up @@ -68,5 +69,6 @@ export const getClassDeclarationInfo = (
return {
name: getClassDeclarationName(declaration),
factory: () => getClassDeclaration(declaration, analyzer),
isExport: isExport(declaration),
};
};
123 changes: 106 additions & 17 deletions packages/labs/analyzer/src/lib/javascript/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,52 @@ import {
PackageInfo,
Declaration,
DeclarationInfo,
ExportMap,
DeclarationMap,
ModuleInfo,
LocalNameOrReference,
} from '../model.js';
import {getClassDeclarationInfo} from './classes.js';
import {getVariableDeclarationInfo} from './variables.js';
import {AbsolutePath, absoluteToPackage} from '../paths.js';
import {AbsolutePath, PackagePath, absoluteToPackage} from '../paths.js';
import {getPackageInfo} from './packages.js';
import {DiagnosticsError} from '../errors.js';
import {getExportReferences, getImportReference} from '../references.js';

/**
* Returns the sourcePath, jsPath, and package.json contents of the containing
* package for the given module path.
*
* This is a minimal subset of module information needed for constructing a
* Reference object for a module.
*/
export const getModuleInfo = (
modulePath: AbsolutePath,
analyzer: AnalyzerInterface,
packageInfo: PackageInfo = getPackageInfo(modulePath, analyzer)
): ModuleInfo => {
// The packageRoot for this module is needed for translating the source file
// path to a package relative path, and the packageName is needed for
// generating references to any symbols in this module.
const {rootDir, packageJson} = packageInfo;
const absJsPath = getJSPathFromSourcePath(
modulePath as AbsolutePath,
analyzer
);
const jsPath =
absJsPath !== undefined
? absoluteToPackage(absJsPath, rootDir)
: ('not/implemented' as PackagePath);
const sourcePath = absoluteToPackage(
analyzer.path.normalize(modulePath) as AbsolutePath,
rootDir
);
return {
jsPath,
sourcePath,
packageJson,
};
};

/**
* Returns an analyzer `Module` model for the given module path.
Expand All @@ -45,23 +84,17 @@ export const getModule = (
if (sourceFile === undefined) {
throw new Error(`Program did not contain a source file for ${modulePath}`);
}
// The packageRoot for this module is needed for translating the source file
// path to a package relative path, and the packageName is needed for
// generating references to any symbols in this module.
const {rootDir, packageJson} = packageInfo;
const sourcePath = absoluteToPackage(
analyzer.path.normalize(modulePath) as AbsolutePath,
rootDir
);
const jsPath = absoluteToPackage(
getJSPathFromSourcePath(modulePath as AbsolutePath, analyzer),
rootDir
);

const dependencies = new Set<AbsolutePath>();
const declarationMap: DeclarationMap = new Map<string, () => Declaration>();
const exportMap: ExportMap = new Map<string, LocalNameOrReference>();
const reexports: ts.Expression[] = [];
const addDeclaration = (info: DeclarationInfo) => {
const {name, factory} = info;
const {name, factory, isExport} = info;
declarationMap.set(name, factory);
if (isExport) {
exportMap.set(name, name);
}
};

// Find and add models for declarations in the module
Expand All @@ -71,6 +104,16 @@ export const getModule = (
addDeclaration(getClassDeclarationInfo(statement, analyzer));
} else if (ts.isVariableStatement(statement)) {
getVariableDeclarationInfo(statement, analyzer).forEach(addDeclaration);
} else if (ts.isExportDeclaration(statement) && !statement.isTypeOnly) {
const {exportClause, moduleSpecifier} = statement;
if (exportClause === undefined && moduleSpecifier !== undefined) {
reexports.push(moduleSpecifier);
} else {
getExportReferences(statement, analyzer).forEach(
({exportName, decNameOrRef}) =>
exportMap.set(exportName, decNameOrRef)
);
}
} else if (ts.isImportDeclaration(statement)) {
dependencies.add(
getPathForModuleSpecifierExpression(statement.moduleSpecifier, analyzer)
Expand All @@ -79,12 +122,12 @@ export const getModule = (
}
// Construct module and save in cache
const module = new Module({
sourcePath,
jsPath,
...getModuleInfo(modulePath, analyzer, packageInfo),
sourceFile,
packageJson,
declarationMap,
dependencies,
exportMap,
finalizeExports: () => finalizeExports(reexports, exportMap, analyzer),
});
analyzer.moduleCache.set(
analyzer.path.normalize(sourceFile.fileName) as AbsolutePath,
Expand All @@ -93,6 +136,27 @@ export const getModule = (
return module;
};

/**
* For any re-exported modules (i.e. `export * from 'foo'`), add all of the
* exported names of the reexported module to the given exportMap, with
* References into that module.
*/
const finalizeExports = (
reexportSpecifiers: ts.Expression[],
exportMap: ExportMap,
analyzer: AnalyzerInterface
) => {
for (const moduleSpecifier of reexportSpecifiers) {
const module = getModule(
getPathForModuleSpecifierExpression(moduleSpecifier, analyzer),
analyzer
);
for (const name of module.exportNames) {
exportMap.set(name, getImportReference(moduleSpecifier, name, analyzer));
}
}
};

/**
* Returns a cached Module model for the given module path if it and all of its
* dependencies' models are still valid since the model was cached. If the
Expand Down Expand Up @@ -156,6 +220,19 @@ const getJSPathFromSourcePath = (
if (sourcePath.endsWith('js')) {
return sourcePath;
}
// TODO(kschaaf): If the source file was a declaration file, this means we're
// likely getting information about an externally imported package that had
// types. In this case, we'll need to update our logic to resolve the import
// specifier to the JS path (in addition to the source file path that we do
// today). Unfortunately, TS's specifier resolver always prefers a declaration
// file, and due to type roots and other tsconfig fancies, it's not
// straightforward to go from a declaration file to a source file. In order to
// properly implement this we'll probably need to bring our own node module
// resolver ala https://www.npmjs.com/package/resolve. That change should be
// done along with the custom-elements.json manifest work.
if (sourcePath.endsWith('.d.ts')) {
return undefined;
}
// Use the TS API to determine where the associated JS will be output based
// on tsconfig settings.
const outputPath = ts
Expand Down Expand Up @@ -194,3 +271,15 @@ export const getPathForModuleSpecifierExpression = (
}
return analyzer.path.normalize(resolvedPath) as AbsolutePath;
};

/**
* Returns the declaration for the named export of the given module path;
* note that if the given module re-exported a declaration from another
* module, references are followed to the concrete declaration, which is
* returned.
*/
export const getExportFromSourcePath = (
modulePath: AbsolutePath,
name: string,
analyzer: AnalyzerInterface
) => getModule(modulePath, analyzer)?.getResolvedExport(name);
2 changes: 2 additions & 0 deletions packages/labs/analyzer/src/lib/javascript/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
AnalyzerInterface,
DeclarationInfo,
} from '../model.js';
import {isExport} from '../references.js';
import {DiagnosticsError} from '../errors.js';
import {getTypeForNode} from '../types.js';

Expand Down Expand Up @@ -68,6 +69,7 @@ const getVariableDeclarationInfoList = (
{
name: name.text,
factory: () => getVariableDeclaration(dec, name, analyzer),
isExport: isExport(dec),
},
];
} else if (
Expand Down
88 changes: 88 additions & 0 deletions packages/labs/analyzer/src/lib/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export class Package extends PackageInfo {
}
}

export type LocalNameOrReference = string | Reference;
export type ExportMap = Map<string, LocalNameOrReference>;
export type DeclarationMap = Map<string, Declaration | (() => Declaration)>;

export interface ModuleInit {
Expand All @@ -82,7 +84,15 @@ export interface ModuleInit {
jsPath: PackagePath;
packageJson: PackageJson;
declarationMap: DeclarationMap;
exportMap: ExportMap;
dependencies: Set<AbsolutePath>;
finalizeExports?: () => void;
}

export interface ModuleInfo {
sourcePath: PackagePath;
jsPath: PackagePath;
packageJson: PackageJson;
}

export interface ModuleInfo {
Expand Down Expand Up @@ -124,13 +134,71 @@ export class Module {
* The package.json contents for the package containing this module.
*/
readonly packageJson: PackageJson;
/**
* A map of exported names to local declaration names or References, in
* the case of re-exported symbols.
*/
private readonly _exportMap: ExportMap;
/**
* A list of module paths for all wildcard re-exports
*/
private _finalizeExports: (() => void) | undefined;

constructor(init: ModuleInit) {
this.sourceFile = init.sourceFile;
this.sourcePath = init.sourcePath;
this.jsPath = init.jsPath;
this.packageJson = init.packageJson;
this._declarationMap = init.declarationMap;
this.dependencies = init.dependencies;
this._exportMap = init.exportMap;
this._finalizeExports = init.finalizeExports;
}

/**
* Ensures the list of exports includes the names of all reexports
* from other modules.
*/
private _ensureExportsFinalized() {
if (this._finalizeExports !== undefined) {
this._finalizeExports();
this._finalizeExports = undefined;
}
}

/**
* Returns names of all exported declarations.
*/
get exportNames() {
this._ensureExportsFinalized();
return Array.from(this._exportMap.keys());
}

/**
* Given an exported symbol name, returns a Declaration if it was
* defined in this module, or a Reference if it was imported from
* another module.
*/
getExport(name: string): Declaration | Reference {
this._ensureExportsFinalized();
const exp = this._exportMap.get(name);
if (exp instanceof Reference) {
return exp;
} else {
return this.getDeclaration(name);
}
}

/**
* Given an exported symbol name, returns the concrete Declaration
* for that symbol, following it through any re-exports.
*/
getResolvedExport(name: string): Declaration {
let exp = this.getExport(name);
while (exp instanceof Reference) {
exp = exp.dereference();
}
return exp as Declaration;
}

/**
Expand Down Expand Up @@ -283,18 +351,22 @@ export interface ReferenceInit {
package?: string | undefined;
module?: string | undefined;
isGlobal?: boolean;
dereference?: () => Declaration | undefined;
}

export class Reference {
readonly name: string;
readonly package: string | undefined;
readonly module: string | undefined;
readonly isGlobal: boolean;
private readonly _dereference: () => Declaration | undefined;
private _model: Declaration | undefined = undefined;
constructor(init: ReferenceInit) {
this.name = init.name;
this.package = init.package;
this.module = init.module;
this.isGlobal = init.isGlobal ?? false;
this._dereference = init.dereference ?? (() => undefined);
}

get moduleSpecifier() {
Expand All @@ -303,6 +375,21 @@ export class Reference {
? undefined
: (this.package || '') + separator + (this.module || '');
}

/**
* Returns the Declaration model that this reference points to, optionally
* validating (and casting) it to be of a given type by passing a model
* constructor.
*/
dereference<T extends Declaration>(type?: Constructor<T> | undefined): T {
const model = (this._model ??= this._dereference());
if (type !== undefined && model !== undefined && !(model instanceof type)) {
throw new Error(
`Expected reference to ${this.name} in module ${this.moduleSpecifier} to be of type ${type.name}`
);
}
return model as T;
}
}

export interface TypeInit {
Expand Down Expand Up @@ -385,4 +472,5 @@ export interface AnalyzerInterface {
export type DeclarationInfo = {
name: string;
factory: () => Declaration;
isExport?: boolean;
};

0 comments on commit 1d37f5d

Please sign in to comment.