Update the license headers throughout the repository to reference Google LLC rather than Google Inc, for the required license headers. PR Close #37205
		
			
				
	
	
		
			134 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			134 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * @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.io/license
 | |
|  */
 | |
| 
 | |
| import {readFileSync} from 'fs';
 | |
| import {dirname, join, resolve} from 'path';
 | |
| import * as ts from 'typescript';
 | |
| 
 | |
| import {getFileStatus} from './file_system';
 | |
| import {getModuleReferences} from './parser';
 | |
| 
 | |
| export type ModuleResolver = (specifier: string) => string|null;
 | |
| 
 | |
| /**
 | |
|  * Reference chains describe a sequence of source files which are connected through imports.
 | |
|  * e.g. `file_a.ts` imports `file_b.ts`, whereas `file_b.ts` imports `file_c.ts`. The reference
 | |
|  * chain data structure could be used to represent this import sequence.
 | |
|  */
 | |
| export type ReferenceChain<T = ts.SourceFile> = T[];
 | |
| 
 | |
| /** Default extensions that the analyzer uses for resolving imports. */
 | |
| const DEFAULT_EXTENSIONS = ['ts', 'js', 'd.ts'];
 | |
| 
 | |
| /**
 | |
|  * Analyzer that can be used to detect import cycles within source files. It supports
 | |
|  * custom module resolution, source file caching and collects unresolved specifiers.
 | |
|  */
 | |
| export class Analyzer {
 | |
|   private _sourceFileCache = new Map<string, ts.SourceFile>();
 | |
| 
 | |
|   unresolvedModules = new Set<string>();
 | |
|   unresolvedFiles = new Map<string, string[]>();
 | |
| 
 | |
|   constructor(
 | |
|       public resolveModuleFn?: ModuleResolver, public extensions: string[] = DEFAULT_EXTENSIONS) {}
 | |
| 
 | |
|   /** Finds all cycles in the specified source file. */
 | |
|   findCycles(sf: ts.SourceFile, visited = new WeakSet<ts.SourceFile>(), path: ReferenceChain = []):
 | |
|       ReferenceChain[] {
 | |
|     const previousIndex = path.indexOf(sf);
 | |
|     // If the given node is already part of the current path, then a cycle has
 | |
|     // been found. Add the reference chain which represents the cycle to the results.
 | |
|     if (previousIndex !== -1) {
 | |
|       return [path.slice(previousIndex)];
 | |
|     }
 | |
|     // If the node has already been visited, then it's not necessary to go check its edges
 | |
|     // again. Cycles would have been already detected and collected in the first check.
 | |
|     if (visited.has(sf)) {
 | |
|       return [];
 | |
|     }
 | |
|     path.push(sf);
 | |
|     visited.add(sf);
 | |
|     // Go through all edges, which are determined through import/exports, and collect cycles.
 | |
|     const result: ReferenceChain[] = [];
 | |
|     for (const ref of getModuleReferences(sf)) {
 | |
|       const targetFile = this._resolveImport(ref, sf.fileName);
 | |
|       if (targetFile !== null) {
 | |
|         result.push(...this.findCycles(this.getSourceFile(targetFile), visited, path.slice()));
 | |
|       }
 | |
|     }
 | |
|     return result;
 | |
|   }
 | |
| 
 | |
|   /** Gets the TypeScript source file of the specified path. */
 | |
|   getSourceFile(filePath: string): ts.SourceFile {
 | |
|     const resolvedPath = resolve(filePath);
 | |
|     if (this._sourceFileCache.has(resolvedPath)) {
 | |
|       return this._sourceFileCache.get(resolvedPath)!;
 | |
|     }
 | |
|     const fileContent = readFileSync(resolvedPath, 'utf8');
 | |
|     const sourceFile =
 | |
|         ts.createSourceFile(resolvedPath, fileContent, ts.ScriptTarget.Latest, false);
 | |
|     this._sourceFileCache.set(resolvedPath, sourceFile);
 | |
|     return sourceFile;
 | |
|   }
 | |
| 
 | |
|   /** Resolves the given import specifier with respect to the specified containing file path. */
 | |
|   private _resolveImport(specifier: string, containingFilePath: string): string|null {
 | |
|     if (specifier.charAt(0) === '.') {
 | |
|       const resolvedPath = this._resolveFileSpecifier(specifier, containingFilePath);
 | |
|       if (resolvedPath === null) {
 | |
|         this._trackUnresolvedFileImport(specifier, containingFilePath);
 | |
|       }
 | |
|       return resolvedPath;
 | |
|     }
 | |
|     if (this.resolveModuleFn) {
 | |
|       const targetFile = this.resolveModuleFn(specifier);
 | |
|       if (targetFile !== null) {
 | |
|         const resolvedPath = this._resolveFileSpecifier(targetFile);
 | |
|         if (resolvedPath !== null) {
 | |
|           return resolvedPath;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     this.unresolvedModules.add(specifier);
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   /** Tracks the given file import as unresolved. */
 | |
|   private _trackUnresolvedFileImport(specifier: string, originFilePath: string) {
 | |
|     if (!this.unresolvedFiles.has(originFilePath)) {
 | |
|       this.unresolvedFiles.set(originFilePath, [specifier]);
 | |
|     }
 | |
|     this.unresolvedFiles.get(originFilePath)!.push(specifier);
 | |
|   }
 | |
| 
 | |
|   /** Resolves the given import specifier to the corresponding source file. */
 | |
|   private _resolveFileSpecifier(specifier: string, containingFilePath?: string): string|null {
 | |
|     const importFullPath =
 | |
|         containingFilePath !== undefined ? join(dirname(containingFilePath), specifier) : specifier;
 | |
|     const stat = getFileStatus(importFullPath);
 | |
|     if (stat && stat.isFile()) {
 | |
|       return importFullPath;
 | |
|     }
 | |
|     for (const extension of this.extensions) {
 | |
|       const pathWithExtension = `${importFullPath}.${extension}`;
 | |
|       const stat = getFileStatus(pathWithExtension);
 | |
|       if (stat && stat.isFile()) {
 | |
|         return pathWithExtension;
 | |
|       }
 | |
|     }
 | |
|     // Directories should be considered last. TypeScript first looks for source files, then
 | |
|     // falls back to directories if no file with appropriate extension could be found.
 | |
|     if (stat && stat.isDirectory()) {
 | |
|       return this._resolveFileSpecifier(join(importFullPath, 'index'));
 | |
|     }
 | |
|     return null;
 | |
|   }
 | |
| }
 |