license import * as path from 'path' ; import * as ts from 'typescript' ; import * as perfTrace from './perf_trace' ; import * as pluginApi from './plugin_api' ; export interface StrictDepsPluginConfig { compilationTargetSrc : string [ ] ; allowedStrictDeps : string [ ] ; rootDir : string ; } /** The TypeScript diagnostic code for "Cannot find module ...". */ export const TS_ERR_CANNOT_FIND_MODULE = 2307 ; /**
 * The strict_deps plugin checks the imports of the compiled modules.
 *
 * It implements strict deps, i.e. enforces that each file in
 * `config.compilationTargetSrc` only imports from files in
 * `config.allowedStrictDeps`.
 *
 * This is used to implement strict dependency checking -
 * source files in a build target may only import sources of their immediate
 * dependencies, but not sources of their transitive dependencies.
 *
 * strict_deps also makes sure that no imports ends in '.ts'. TypeScript
 * allows imports including the file extension, but our runtime loading support
 * fails with it.
 *
 * strict_deps currently does not check ambient/global definitions.
 */ export class Plugin implements pluginApi . DiagnosticPlugin { constructor ( private readonly program , private readonly config ) { } readonly name = 'strictDeps' ; getDiagnostics ( sourceFile ) { return checkModuleDeps ( sourceFile , this . program . getTypeChecker ( ) , this . config . allowedStrictDeps , this . config . rootDir ) ; } } export function checkModuleDeps ( sf , tc , allowedDeps , rootDir ) { function stripExt ( fn ) { return fn . replace ( /(\.d)?\.tsx?$/ , '' ) ; } const allowedMap = { } ; for ( const d of allowedDeps ) allowedMap [ stripExt ( d ) ] = true ; const result = [ ] ; for ( const stmt of sf . statements ) { if ( stmt . kind !== ts . SyntaxKind . ImportDeclaration && stmt . kind !== ts . SyntaxKind . ExportDeclaration ) { continue ; } const id = stmt as ts . ImportDeclaration | ts . ExportDeclaration ; const modSpec = id . moduleSpecifier ; if ( ! modSpec ) continue ; const sym = tc . getSymbolAtLocation ( modSpec ) ; if ( ! sym || ! sym . declarations || sym . declarations . length < 1 ) { continue ; } const declFileNames = sym . declarations . map ( decl => decl . getSourceFile ( ) . fileName ) ; if ( declFileNames . find ( declFileName => ! ! allowedMap [ stripExt ( declFileName ) ] ) ) { continue ; } const importNames = declFileNames . map ( declFileName => path . posix . relative ( rootDir , declFileName ) ) ; const extraDeclarationLocationsMessage = ( importNames . length < 2 ) ? '' : `(It is also declared in ${ importNames . slice ( 1 ) . join ( ', ' ) }) ` ; result . push ( { file : sf , start : modSpec . getStart ( ) , length : modSpec . getEnd ( ) - modSpec . getStart ( ) , messageText : `transitive dependency on ${ importNames [ 0 ] } not allowed. ` + extraDeclarationLocationsMessage + `Please add the BUILD target to your rule's deps.` , category : ts . DiagnosticCategory . Error , code : TS_ERR_CANNOT_FIND_MODULE } ) ; } return result ; }