@@ -75,12 +75,24 @@ const NAMESPACE_IMPORT = /import\s+\*\s+as\s+([\w$]+)\s+from\s*['"]([^'"]+)['"]/
7575/** One export of a module, and the expression the importing module reaches it through. */
7676interface ExportReference {
7777 exported : string
78- /** The local binding, or `namespace.member` for a read through a namespace import. */
79- local : string
78+ /**
79+ * The local binding, or `namespace.member` for a member read; absent when a
80+ * namespace escapes member access, where the read cannot be traced.
81+ */
82+ local ?: string
8083}
8184
82- /** The exports of `from` a module reaches as values, through named or namespace imports. */
83- function exportReferences ( source : string , from : string ) : ExportReference [ ] {
85+ /**
86+ * The exports of `from` a module reaches as values, through named or namespace
87+ * imports. A namespace used other than as `namespace.member` — destructured,
88+ * indexed, or passed along — is taken to read every one of `exports`, so an
89+ * untraceable read is never mistaken for none.
90+ */
91+ function exportReferences (
92+ source : string ,
93+ from : string ,
94+ exports : readonly string [ ]
95+ ) : ExportReference [ ] {
8496 const references : ExportReference [ ] = [ ]
8597 for ( const [ , typeOnly , specifiers , module ] of source . matchAll ( NAMED_IMPORT ) ) {
8698 if ( module !== from || typeOnly ) continue
@@ -89,25 +101,30 @@ function exportReferences(source: string, from: string): ExportReference[] {
89101 if ( exported && ! exported . startsWith ( 'type ' ) ) references . push ( { exported, local } )
90102 }
91103 }
92- for ( const [ , namespace , module ] of source . matchAll ( NAMESPACE_IMPORT ) ) {
104+ for ( const [ declaration , namespace , module ] of source . matchAll ( NAMESPACE_IMPORT ) ) {
93105 if ( module !== from ) continue
94- const memberRead = new RegExp ( `(?<![\\w$.])${ escapeRegExp ( namespace ) } \\.( [\\w$]+)` , 'g' )
95- for ( const [ local , exported ] of source . matchAll ( memberRead ) ) {
106+ const binding = `(?<![\\w$.])${ escapeRegExp ( namespace ) } (?! [\\w$])`
107+ for ( const [ local , exported ] of source . matchAll ( new RegExp ( ` ${ binding } \\.([\\w$]+)` , 'g' ) ) ) {
96108 references . push ( { exported, local } )
97109 }
110+ const body = source . replace ( declaration , '' )
111+ if ( new RegExp ( `${ binding } (?!\\s*\\.[\\w$])` ) . test ( body ) ) {
112+ references . push ( ...exports . map ( ( exported ) => ( { exported } ) ) )
113+ }
98114 }
99115 return references
100116}
101117
102- function isCalled ( source : string , local : string ) : boolean {
118+ function isCalled ( source : string , local : string | undefined ) : boolean {
119+ if ( local === undefined ) return false
103120 return new RegExp ( `(?<![\\w$.])${ escapeRegExp ( local ) } \\s*\\(` ) . test ( source )
104121}
105122
106123/** The raw catalog exports a module reads. */
107124function rawCatalogReads ( source : string ) : string [ ] {
108125 const names = new Set < string > ( )
109126 for ( const [ module , catalog ] of Object . entries ( RAW_CATALOGS ) ) {
110- for ( const { exported } of exportReferences ( source , module ) ) {
127+ for ( const { exported } of exportReferences ( source , module , catalog ) ) {
111128 if ( catalog . includes ( exported ) ) names . add ( exported )
112129 }
113130 }
@@ -127,7 +144,7 @@ export function findIntegrationAccessViolations(
127144 const violations : IntegrationAccessViolation [ ] = [ ]
128145 const usedExemptions = new Set < string > ( )
129146 for ( const { path, source } of files ) {
130- const gateReferences = exportReferences ( source , GATE_MODULE )
147+ const gateReferences = exportReferences ( source , GATE_MODULE , [ GATE_NAME , FAIL_OPEN_NAME ] )
131148 const readsFailOpen = gateReferences . some ( ( { exported } ) => exported === FAIL_OPEN_NAME )
132149 if ( readsFailOpen && path !== FAIL_OPEN_READER ) {
133150 violations . push ( { path, names : [ FAIL_OPEN_NAME ] , stale : false } )
0 commit comments