@@ -95,11 +95,85 @@ class State {
9595
9696 /** Next response to be delivered. */
9797 public pendingResponse : string = null ;
98+
99+ /** Map from `package.json` files to their contents. */
100+ public parsedPackageJson = new Map < string , any > ( ) ;
101+
102+ /** Map from `package.json` files to the file referenced in its `types` or `typings` field. */
103+ public packageTypings = new Map < string , string | undefined > ( ) ;
104+
105+ /** Map from file path to the enclosing `package.json` file, if any. Will not traverse outside node_modules. */
106+ public enclosingPackageJson = new Map < string , string | undefined > ( ) ;
98107}
99108let state = new State ( ) ;
100109
101110const reloadMemoryThresholdMb = getEnvironmentVariable ( "SEMMLE_TYPESCRIPT_MEMORY_THRESHOLD" , Number , 1000 ) ;
102111
112+ function getPackageJson ( file : string ) : any {
113+ let cache = state . parsedPackageJson ;
114+ if ( cache . has ( file ) ) return cache . get ( file ) ;
115+ let result = getPackageJsonRaw ( file ) ;
116+ cache . set ( file , result ) ;
117+ return result ;
118+ }
119+
120+ function getPackageJsonRaw ( file : string ) : any {
121+ if ( ! ts . sys . fileExists ( file ) ) return undefined ;
122+ try {
123+ let json = JSON . parse ( ts . sys . readFile ( file ) ) ;
124+ if ( typeof json !== 'object' ) return undefined ;
125+ return json ;
126+ } catch ( e ) {
127+ return undefined ;
128+ }
129+ }
130+
131+ function getPackageTypings ( file : string ) : string | undefined {
132+ let cache = state . packageTypings ;
133+ if ( cache . has ( file ) ) return cache . get ( file ) ;
134+ let result = getPackageTypingsRaw ( file ) ;
135+ cache . set ( file , result ) ;
136+ return result ;
137+ }
138+
139+ function getPackageTypingsRaw ( packageJsonFile : string ) : string | undefined {
140+ let json = getPackageJson ( packageJsonFile ) ;
141+ if ( json == null ) return undefined ;
142+ let typings = json . types || json . typings ; // "types" and "typings" are aliases
143+ if ( typeof typings !== 'string' ) return undefined ;
144+ let absolutePath = pathlib . join ( pathlib . dirname ( packageJsonFile ) , typings ) ;
145+ if ( ts . sys . directoryExists ( absolutePath ) ) {
146+ absolutePath = pathlib . join ( absolutePath , 'index.d.ts' ) ;
147+ } else if ( ! absolutePath . endsWith ( '.ts' ) ) {
148+ absolutePath += '.d.ts' ;
149+ }
150+ if ( ! ts . sys . fileExists ( absolutePath ) ) return undefined ;
151+ return ts . sys . resolvePath ( absolutePath ) ;
152+ }
153+
154+ function getEnclosingPackageJson ( file : string ) : string | undefined {
155+ let cache = state . packageTypings ;
156+ if ( cache . has ( file ) ) return cache . get ( file ) ;
157+ let result = getEnclosingPackageJsonRaw ( file ) ;
158+ cache . set ( file , result ) ;
159+ return result ;
160+ }
161+
162+ function getEnclosingPackageJsonRaw ( file : string ) : string | undefined {
163+ let packageJson = pathlib . join ( file , 'package.json' ) ;
164+ if ( ts . sys . fileExists ( packageJson ) ) {
165+ return packageJson ;
166+ }
167+ if ( pathlib . basename ( file ) === 'node_modules' ) {
168+ return undefined ;
169+ }
170+ let dirname = pathlib . dirname ( file ) ;
171+ if ( dirname . length < file . length ) {
172+ return getEnclosingPackageJson ( dirname ) ;
173+ }
174+ return undefined ;
175+ }
176+
103177/**
104178 * Debugging method for finding cycles in the TypeScript AST. Should not be used in production.
105179 *
@@ -505,14 +579,18 @@ function handleOpenProjectCommand(command: OpenProjectCommand) {
505579 // inverse mapping, nor a way to enumerate all known module names. So we discover all
506580 // modules on the type roots (usually "node_modules/@types" but this is configurable).
507581 let typeRoots = ts . getEffectiveTypeRoots ( config . options , {
508- directoryExists : ( path ) => fs . existsSync ( path ) ,
582+ directoryExists : ( path ) => ts . sys . directoryExists ( path ) ,
509583 getCurrentDirectory : ( ) => basePath ,
510584 } ) ;
511585
512586 for ( let typeRoot of typeRoots || [ ] ) {
513- if ( fs . existsSync ( typeRoot ) && fs . statSync ( typeRoot ) . isDirectory ( ) ) {
587+ if ( ts . sys . directoryExists ( typeRoot ) ) {
514588 traverseTypeRoot ( typeRoot , "" ) ;
515589 }
590+ let virtualTypeRoot = virtualSourceRoot . toVirtualPathIfDirectoryExists ( typeRoot ) ;
591+ if ( virtualTypeRoot != null ) {
592+ traverseTypeRoot ( virtualTypeRoot , "" ) ;
593+ }
516594 }
517595
518596 for ( let sourceFile of program . getSourceFiles ( ) ) {
@@ -549,22 +627,25 @@ function handleOpenProjectCommand(command: OpenProjectCommand) {
549627 if ( sourceFile == null ) {
550628 continue ;
551629 }
552- addModuleBindingFromRelativePath ( sourceFile , importPrefix , child ) ;
630+ let importPath = getImportPathFromFileInFolder ( importPrefix , child ) ;
631+ addModuleBindingFromImportPath ( sourceFile , importPath ) ;
553632 }
554633 }
555634
635+ function getImportPathFromFileInFolder ( folder : string , baseName : string ) {
636+ let stem = getStem ( baseName ) ;
637+ return ( stem === "index" )
638+ ? folder
639+ : joinModulePath ( folder , stem ) ;
640+ }
641+
556642 /**
557643 * Emits module bindings for a module with relative path `folder/baseName`.
558644 */
559- function addModuleBindingFromRelativePath ( sourceFile : ts . SourceFile , folder : string , baseName : string ) {
645+ function addModuleBindingFromImportPath ( sourceFile : ts . SourceFile , importPath : string ) {
560646 let symbol = typeChecker . getSymbolAtLocation ( sourceFile ) ;
561647 if ( symbol == null ) return ; // Happens if the source file is not a module.
562648
563- let stem = getStem ( baseName ) ;
564- let importPath = ( stem === "index" )
565- ? folder
566- : joinModulePath ( folder , stem ) ;
567-
568649 let canonicalSymbol = getEffectiveExportTarget ( symbol ) ; // Follow `export = X` declarations.
569650 let symbolId = state . typeTable . getSymbolId ( canonicalSymbol ) ;
570651
@@ -576,7 +657,7 @@ function handleOpenProjectCommand(command: OpenProjectCommand) {
576657 // Note: the `globalExports` map is stored on the original symbol, not the target of `export=`.
577658 if ( symbol . globalExports != null ) {
578659 symbol . globalExports . forEach ( ( global : ts . Symbol ) => {
579- state . typeTable . addGlobalMapping ( symbolId , global . name ) ;
660+ state . typeTable . addGlobalMapping ( symbolId , global . name ) ;
580661 } ) ;
581662 }
582663 }
@@ -605,11 +686,30 @@ function handleOpenProjectCommand(command: OpenProjectCommand) {
605686 let fullPath = sourceFile . fileName ;
606687 let index = fullPath . lastIndexOf ( '/node_modules/' ) ;
607688 if ( index === - 1 ) return ;
689+
608690 let relativePath = fullPath . substring ( index + '/node_modules/' . length ) ;
691+
609692 // Ignore node_modules/@types folders here as they are typically handled as type roots.
610693 if ( relativePath . startsWith ( "@types/" ) ) return ;
694+
695+ // If the enclosing package has a "typings" field, only add module bindings for that file.
696+ let packageJsonFile = getEnclosingPackageJson ( fullPath ) ;
697+ if ( packageJsonFile != null ) {
698+ let json = getPackageJson ( packageJsonFile ) ;
699+ let typings = getPackageTypings ( packageJsonFile ) ;
700+ if ( json != null && typings != null ) {
701+ let name = json . name ;
702+ if ( typings === fullPath && typeof name === 'string' ) {
703+ addModuleBindingFromImportPath ( sourceFile , name ) ;
704+ } else if ( typings != null ) {
705+ return ; // Typings field prevents access to other files in package.
706+ }
707+ }
708+ }
709+
710+ // Add module bindings relative to package directory.
611711 let { dir, base } = pathlib . parse ( relativePath ) ;
612- addModuleBindingFromRelativePath ( sourceFile , dir , base ) ;
712+ addModuleBindingFromImportPath ( sourceFile , getImportPathFromFileInFolder ( dir , base ) ) ;
613713 }
614714
615715 /**
0 commit comments