Skip to content

Commit

Permalink
Refactor internal dep graph & module resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
webpro committed May 22, 2024
1 parent 6616fe1 commit f5faf52
Show file tree
Hide file tree
Showing 20 changed files with 288 additions and 179 deletions.
2 changes: 2 additions & 0 deletions packages/knip/fixtures/import-named-default-id/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import utilOne from './utils';
utilOne();
3 changes: 3 additions & 0 deletions packages/knip/fixtures/import-named-default-id/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "@fixtures/import-named-default-id"
}
3 changes: 3 additions & 0 deletions packages/knip/fixtures/import-named-default-id/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const utilOne = () => console.log('utilOne');
const utilTwo = () => console.log('utilTwo');
export default utilTwo;
84 changes: 29 additions & 55 deletions packages/knip/src/ProjectPrincipal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import type {
SerializableMap,
UnresolvedImport,
} from './types/serializable-map.js';
import type { BoundSourceFile, ProgramMaybe53 } from './typescript/SourceFile.js';
import type { BoundSourceFile } from './typescript/SourceFile.js';
import type { SourceFileManager } from './typescript/SourceFileManager.js';
import { createHosts } from './typescript/createHosts.js';
import { type GetImportsAndExportsOptions, _getImportsAndExports } from './typescript/getImportsAndExports.js';
import type { createCustomModuleResolver } from './typescript/resolveModuleNames.js';
import type { ResolveModuleNames } from './typescript/resolveModuleNames.js';
import { timerify } from './util/Performance.js';
import { compact } from './util/array.js';
import { isStartsLikePackageName, sanitizeSpecifier } from './util/modules.js';
Expand Down Expand Up @@ -79,8 +79,8 @@ export class ProjectPrincipal {
backend: {
fileManager: SourceFileManager;
compilerHost: ts.CompilerHost;
resolveModuleNames: ReturnType<typeof createCustomModuleResolver>;
program?: ProgramMaybe53;
resolveModuleNames: ResolveModuleNames;
program?: ts.Program;
typeChecker?: ts.TypeChecker;
languageServiceHost: ts.LanguageServiceHost;
};
Expand Down Expand Up @@ -219,21 +219,6 @@ export class ProjectPrincipal {
return Array.from(this.projectPaths).filter(filePath => !sourceFiles.has(filePath));
}

private getResolvedModuleHandler(sourceFile: BoundSourceFile) {
const getResolvedModule = this.backend.program?.getResolvedModule;
const resolver = getResolvedModule
? (specifier: string) => getResolvedModule(sourceFile, specifier, /* mode */ undefined)
: (specifier: string) => sourceFile.resolvedModules?.get(specifier, /* mode */ undefined);
if (!this.isWatch) return resolver;

// TODO It's either this awkward bit in watch mode to handle deleted files, or some large refactoring
return (specifier: string) => {
const m = resolver(specifier);
if (m?.resolvedModule?.resolvedFileName && this.deletedFiles.has(m.resolvedModule.resolvedFileName)) return;
return m;
};
}

public analyzeSourceFile(filePath: string, options: Omit<GetImportsAndExportsOptions, 'skipExports'>) {
const fd = this.cache.getFileDescriptor(filePath);
if (!fd.changed && fd.meta?.data) return deserialize(fd.meta.data);
Expand All @@ -247,45 +232,38 @@ export class ProjectPrincipal {

const skipExports = this.skipExportsAnalysis.has(filePath);

const { imports, exports, scripts } = _getImportsAndExports(
sourceFile,
this.getResolvedModuleHandler(sourceFile),
this.backend.typeChecker,
{ ...options, skipExports }
);
const resolve = (specifier: string) => this.backend.resolveModuleNames([specifier], sourceFile.fileName)[0];

const { internal, unresolved, external } = imports;
const { imports, exports, scripts } = _getImportsAndExports(sourceFile, resolve, this.backend.typeChecker, {
...options,
skipExports,
});

const { internal, resolved, unresolved, external } = imports;

const unresolvedImports = new Set<UnresolvedImport>();

for (const filePath of resolved) {
const isIgnored = this.isGitIgnored(filePath);
if (!isIgnored) this.addEntryPath(filePath, { skipExportsAnalysis: true });
}

for (const unresolvedImport of unresolved) {
const { specifier } = unresolvedImport;
if (specifier.startsWith('http')) {
// Ignore Deno style http import specifiers.
continue;
}
const resolvedModule = this.resolveModule(specifier, filePath);
if (resolvedModule) {
if (resolvedModule.isExternalLibraryImport) {
const sanitizedSpecifier = sanitizeSpecifier(specifier);
external.add(sanitizedSpecifier);
} else {
const isIgnored = this.isGitIgnored(resolvedModule.resolvedFileName);
if (!isIgnored) this.addEntryPath(resolvedModule.resolvedFileName, { skipExportsAnalysis: true });
}

// Ignore Deno style http import specifiers
if (specifier.startsWith('http')) continue;

const sanitizedSpecifier = sanitizeSpecifier(specifier);
if (isStartsLikePackageName(sanitizedSpecifier)) {
external.add(sanitizedSpecifier);
} else {
const sanitizedSpecifier = sanitizeSpecifier(specifier);
if (isStartsLikePackageName(sanitizedSpecifier)) {
// Should never end up here; maybe a dependency that was not installed.
external.add(sanitizedSpecifier);
} else {
const isIgnored = this.isGitIgnored(join(dirname(filePath), sanitizedSpecifier));
if (!isIgnored) {
const ext = extname(sanitizedSpecifier);
const hasIgnoredExtension = FOREIGN_FILE_EXTENSIONS.has(ext);
if (!ext || (ext !== '.json' && !hasIgnoredExtension)) {
unresolvedImports.add(unresolvedImport);
}
const isIgnored = this.isGitIgnored(join(dirname(filePath), sanitizedSpecifier));
if (!isIgnored) {
const ext = extname(sanitizedSpecifier);
const hasIgnoredExtension = FOREIGN_FILE_EXTENSIONS.has(ext);
if (!ext || (ext !== '.json' && !hasIgnoredExtension)) {
unresolvedImports.add(unresolvedImport);
}
}
}
Expand All @@ -307,10 +285,6 @@ export class ProjectPrincipal {
this.backend.fileManager.sourceFileCache.delete(filePath);
}

public resolveModule(specifier: string, filePath: string = specifier) {
return this.backend.resolveModuleNames([specifier], filePath)[0];
}

public findUnusedMembers(filePath: string, members: SerializableExportMember[]) {
if (!this.findReferences) {
const languageService = ts.createLanguageService(this.backend.languageServiceHost, ts.createDocumentRegistry());
Expand Down
29 changes: 25 additions & 4 deletions packages/knip/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,12 +252,33 @@ export const main = async (unresolvedConfiguration: CommandLineOptions) => {

const updateImports = (importedModule: SerializableImports, importItems: SerializableImports) => {
for (const id of importItems.refs) importedModule.refs.add(id);
for (const id of importItems.imported) importedModule.imported.add(id);
for (const id of importItems.importedAs) importedModule.importedAs.add(id);
for (const id of importItems.importedNs) importedModule.importedNs.add(id);
for (const id of importItems.isReExportedBy) importedModule.isReExportedBy.add(id);
for (const id of importItems.isReExportedNs) importedModule.isReExportedNs.add(id);
if (importItems.hasStar) importedModule.hasStar = true;
if (importItems.isReExport) importedModule.isReExport = true;

for (const [id, value] of importItems.reExportedNs.entries()) {
if (importedModule.reExportedNs.has(id)) {
for (const v of value) importedModule.reExportedNs.get(id)?.add(v);
} else {
importedModule.reExportedNs.set(id, value);
}
}

for (const [id, value] of importItems.reExportedAs.entries()) {
if (importedModule.reExportedAs.has(id)) {
for (const v of value) importedModule.reExportedAs.get(id)?.add(v);
} else {
importedModule.reExportedAs.set(id, value);
}
}

for (const [id, value] of importItems.reExportedBy.entries()) {
if (importedModule.reExportedBy.has(id)) {
for (const v of value) importedModule.reExportedBy.get(id)?.add(v);
} else {
importedModule.reExportedBy.set(id, value);
}
}
};

const updateImported = (filePath: string, importItems: SerializableImports) => {
Expand Down
1 change: 1 addition & 0 deletions packages/knip/src/types/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export interface ImportNode {
symbol?: ts.Symbol;
isTypeOnly?: boolean;
isReExport?: boolean;
resolve?: boolean;
}
9 changes: 4 additions & 5 deletions packages/knip/src/types/serializable-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ type Tags = Set<string>;
export type SerializableImports = {
specifier: Specifier;
refs: References;
hasStar: boolean;
imported: Set<string>;
importedAs: Set<[string, string]>;
importedNs: Set<string>;
isReExport: boolean;
isReExportedBy: Set<string>;
isReExportedAs: Set<[string, string]>;
isReExportedNs: Set<[string, string]>;
reExportedBy: Map<string, Set<string>>;
reExportedAs: Map<string, Set<[string, string]>>;
reExportedNs: Map<string, Set<string>>;
};

export type SerializableImportMap = Record<FilePath, SerializableImports>;
Expand Down
11 changes: 0 additions & 11 deletions packages/knip/src/typescript/SourceFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,3 @@ export interface BoundSourceFile extends ts.SourceFile {

pragmas?: Map<string, PragmaMap | PragmaMap[]>;
}

export interface ProgramMaybe53 extends ts.Program {
// Only available in TypeScript =>5.3.0
getResolvedModule?: (
sourceFile: ts.SourceFile,
moduleName: string,
mode: ts.ResolutionMode
) => ts.ResolvedModuleWithFailedLookupLocations | undefined;
}

export type GetResolvedModule = (name: string) => ts.ResolvedModuleWithFailedLookupLocations | undefined;
Loading

0 comments on commit f5faf52

Please sign in to comment.