@@ -12,6 +12,7 @@ const {
1212 getImportAttributes
1313} = require ( "../javascript/JavascriptParser" ) ;
1414const InnerGraph = require ( "../optimize/InnerGraph" ) ;
15+ const AppendOnlyStackedSet = require ( "../util/AppendOnlyStackedSet" ) ;
1516const ConstDependency = require ( "./ConstDependency" ) ;
1617const HarmonyAcceptDependency = require ( "./HarmonyAcceptDependency" ) ;
1718const HarmonyAcceptImportDependency = require ( "./HarmonyAcceptImportDependency" ) ;
@@ -36,9 +37,18 @@ const { ImportPhaseUtils, createGetImportPhase } = require("./ImportPhase");
3637/** @typedef {import("../javascript/JavascriptParser").Members } Members */
3738/** @typedef {import("../javascript/JavascriptParser").MembersOptionals } MembersOptionals */
3839/** @typedef {import("./HarmonyImportDependency").Ids } Ids */
40+ /** @typedef {import("./HarmonyImportDependency").ExportPresenceMode } ExportPresenceMode */
3941/** @typedef {import("./ImportPhase").ImportPhaseType } ImportPhaseType */
4042
43+ /**
44+ * @typedef {object } HarmonySpecifierGuards
45+ * @property {AppendOnlyStackedSet<string> | undefined } guards
46+ */
47+
48+ /** @typedef {Map<string, Set<string>> } Guards Map of import root to guarded member keys */
49+
4150const harmonySpecifierTag = Symbol ( "harmony import" ) ;
51+ const harmonySpecifierGuardTag = Symbol ( "harmony import guard" ) ;
4252
4353/**
4454 * @typedef {object } HarmonySettings
@@ -53,6 +63,18 @@ const harmonySpecifierTag = Symbol("harmony import");
5363
5464const PLUGIN_NAME = "HarmonyImportDependencyParserPlugin" ;
5565
66+ /** @type {(members: Members) => string } */
67+ const getMembersKey = ( members ) => members . join ( "." ) ;
68+
69+ /**
70+ * Strip the root binding name if needed
71+ * @param {HarmonySettings } settings settings
72+ * @param {Ids } ids ids
73+ * @returns {Ids } ids for presence check
74+ */
75+ const getIdsForPresence = ( settings , ids ) =>
76+ settings . ids . length ? ids . slice ( 1 ) : ids ;
77+
5678module . exports = class HarmonyImportDependencyParserPlugin {
5779 /**
5880 * @param {JavascriptParserOptions } options options
@@ -70,13 +92,30 @@ module.exports = class HarmonyImportDependencyParserPlugin {
7092 this . strictThisContextOnImports = options . strictThisContextOnImports ;
7193 }
7294
95+ /**
96+ * @param {JavascriptParser } parser the parser
97+ * @param {Ids } ids ids
98+ * @returns {ExportPresenceMode } exportPresenceMode
99+ */
100+ getExportPresenceMode ( parser , ids ) {
101+ const harmonySettings = /** @type {HarmonySettings= } */ (
102+ parser . currentTagData
103+ ) ;
104+ if ( ! harmonySettings ) return this . exportPresenceMode ;
105+
106+ const data = /** @type {HarmonySpecifierGuards= } */ (
107+ parser . getTagData ( harmonySettings . name , harmonySpecifierGuardTag )
108+ ) ;
109+ return data && data . guards && data . guards . has ( getMembersKey ( ids ) )
110+ ? false
111+ : this . exportPresenceMode ;
112+ }
113+
73114 /**
74115 * @param {JavascriptParser } parser the parser
75116 * @returns {void }
76117 */
77118 apply ( parser ) {
78- const { exportPresenceMode } = this ;
79-
80119 const getImportPhase = createGetImportPhase ( this . options . deferImport ) ;
81120
82121 /**
@@ -228,14 +267,18 @@ module.exports = class HarmonyImportDependencyParserPlugin {
228267 . for ( harmonySpecifierTag )
229268 . tap ( PLUGIN_NAME , ( expr ) => {
230269 const settings = /** @type {HarmonySettings } */ ( parser . currentTagData ) ;
270+
231271 const dep = new HarmonyImportSpecifierDependency (
232272 settings . source ,
233273 settings . sourceOrder ,
234274 settings . ids ,
235275 settings . name ,
236276 /** @type {Range } */
237277 ( expr . range ) ,
238- exportPresenceMode ,
278+ this . getExportPresenceMode (
279+ parser ,
280+ getIdsForPresence ( settings , settings . ids )
281+ ) ,
239282 settings . phase ,
240283 settings . attributes ,
241284 [ ]
@@ -285,7 +328,10 @@ module.exports = class HarmonyImportDependencyParserPlugin {
285328 settings . name ,
286329 /** @type {Range } */
287330 ( expr . range ) ,
288- exportPresenceMode ,
331+ this . getExportPresenceMode (
332+ parser ,
333+ getIdsForPresence ( settings , ids )
334+ ) ,
289335 settings . phase ,
290336 settings . attributes ,
291337 ranges
@@ -335,7 +381,10 @@ module.exports = class HarmonyImportDependencyParserPlugin {
335381 ids ,
336382 settings . name ,
337383 /** @type {Range } */ ( expr . range ) ,
338- exportPresenceMode ,
384+ this . getExportPresenceMode (
385+ parser ,
386+ getIdsForPresence ( settings , ids )
387+ ) ,
339388 settings . phase ,
340389 settings . attributes ,
341390 ranges
@@ -402,7 +451,219 @@ module.exports = class HarmonyImportDependencyParserPlugin {
402451 parser . state . module . addDependency ( dep ) ;
403452 }
404453 } ) ;
454+
455+ /**
456+ * @param {Expression } expression expression
457+ * @returns {{ root: string, members: Members } | undefined } info
458+ */
459+ const getHarmonyImportInfo = ( expression ) => {
460+ const nameInfo = parser . getNameForExpression ( expression ) ;
461+ if ( ! nameInfo ) return ;
462+
463+ const rootInfo = nameInfo . rootInfo ;
464+ const root =
465+ typeof rootInfo === "string"
466+ ? rootInfo
467+ : rootInfo instanceof VariableInfo
468+ ? rootInfo . name
469+ : undefined ;
470+ if ( ! root ) return ;
471+ if ( ! parser . getTagData ( root , harmonySpecifierTag ) ) return ;
472+ return { root, members : nameInfo . getMembers ( ) } ;
473+ } ;
474+
475+ /**
476+ * @param {Guards } guards guards
477+ * @param {string } root root name
478+ * @param {Members } members members
479+ */
480+ const addToGuards = ( guards , root , members ) => {
481+ const membersKey = getMembersKey ( members ) ;
482+ const guardedMembers = guards . get ( root ) ;
483+ if ( guardedMembers ) {
484+ guardedMembers . add ( membersKey ) ;
485+ return ;
486+ }
487+
488+ guards . set (
489+ root ,
490+ // Adding `foo.bar` implies guarding `foo` as well
491+ membersKey === "" ? new Set ( [ "" ] ) : new Set ( [ membersKey , "" ] )
492+ ) ;
493+ } ;
494+
495+ /**
496+ * @param {Expression } expression expression
497+ * @param {Guards } guards guards
498+ * @param {boolean } needTruthy need to be truthy
499+ */
500+ const collect = ( expression , guards , needTruthy ) => {
501+ // !foo
502+ if (
503+ expression . type === "UnaryExpression" &&
504+ expression . operator === "!"
505+ ) {
506+ collect ( expression . argument , guards , ! needTruthy ) ;
507+ return ;
508+ } else if ( expression . type === "LogicalExpression" && needTruthy ) {
509+ // foo && bar
510+ if ( expression . operator === "&&" ) {
511+ collect ( expression . left , guards , true ) ;
512+ collect ( expression . right , guards , true ) ;
513+ }
514+ // falsy || foo
515+ else if ( expression . operator === "||" ) {
516+ const leftEvaluation = parser . evaluateExpression ( expression . left ) ;
517+ const leftBool = leftEvaluation . asBool ( ) ;
518+ if ( leftBool === false ) {
519+ collect ( expression . right , guards , true ) ;
520+ }
521+ }
522+ // nullish ?? foo
523+ else if ( expression . operator === "??" ) {
524+ const leftEvaluation = parser . evaluateExpression ( expression . left ) ;
525+ const leftNullish = leftEvaluation . asNullish ( ) ;
526+ if ( leftNullish === true ) {
527+ collect ( expression . right , guards , true ) ;
528+ }
529+ }
530+ return ;
531+ }
532+ if ( ! needTruthy ) return ;
533+
534+ /**
535+ * @param {Expression } targetExpression expression
536+ * @returns {boolean } is added
537+ */
538+ const addGuardForExpression = ( targetExpression ) => {
539+ const info = getHarmonyImportInfo ( targetExpression ) ;
540+ if ( ! info ) return false ;
541+ addToGuards ( guards , info . root , info . members ) ;
542+ return true ;
543+ } ;
544+
545+ /**
546+ * @param {Expression } left left expression
547+ * @param {Expression } right right expression
548+ * @param {(evaluation: ReturnType<JavascriptParser["evaluateExpression"]>) => boolean } matcher matcher
549+ * @returns {boolean } is added
550+ */
551+ const addGuardForNullishCompare = ( left , right , matcher ) => {
552+ const leftEval = parser . evaluateExpression ( left ) ;
553+ if ( leftEval && matcher ( leftEval ) ) {
554+ return addGuardForExpression ( right ) ;
555+ }
556+ const rightEval = parser . evaluateExpression ( right ) ;
557+ if ( rightEval && matcher ( rightEval ) ) {
558+ return addGuardForExpression ( /** @type {Expression } */ ( left ) ) ;
559+ }
560+ return false ;
561+ } ;
562+
563+ if ( expression . type === "BinaryExpression" ) {
564+ // "bar" in foo
565+ if ( expression . operator === "in" ) {
566+ const leftEvaluation = parser . evaluateExpression ( expression . left ) ;
567+ if ( leftEvaluation . couldHaveSideEffects ( ) ) return ;
568+ const propertyName = leftEvaluation . asString ( ) ;
569+ if ( ! propertyName ) return ;
570+ parser . evaluateExpression ( expression . right ) ;
571+ const info = getHarmonyImportInfo ( expression . right ) ;
572+ if ( ! info ) return ;
573+
574+ if ( info . members . length ) {
575+ for ( const member of info . members ) {
576+ addToGuards ( guards , info . root , [ member ] ) ;
577+ }
578+ }
579+ addToGuards ( guards , info . root , [ ...info . members , propertyName ] ) ;
580+ return ;
581+ }
582+ // foo !== undefined
583+ else if (
584+ expression . operator === "!==" &&
585+ addGuardForNullishCompare (
586+ /** @type {Expression } */ ( expression . left ) ,
587+ expression . right ,
588+ ( evaluation ) => evaluation . isUndefined ( )
589+ )
590+ ) {
591+ return ;
592+ }
593+ // foo != undefined
594+ // foo != null
595+ else if (
596+ expression . operator === "!=" &&
597+ addGuardForNullishCompare (
598+ /** @type {Expression } */ ( expression . left ) ,
599+ expression . right ,
600+ ( evaluation ) => Boolean ( evaluation . asNullish ( ) )
601+ )
602+ ) {
603+ return ;
604+ }
605+ }
606+ addGuardForExpression ( expression ) ;
607+ } ;
608+
609+ /**
610+ * @param {Guards } guards guards
611+ * @param {() => void } walk walk callback
612+ * @returns {void }
613+ */
614+ const withGuards = ( guards , walk ) => {
615+ const applyGuards = ( ) => {
616+ /** @type {(() => void)[] } */
617+ const restoreFns = [ ] ;
618+
619+ for ( const [ rootName , members ] of guards ) {
620+ const previous = parser . getVariableInfo ( rootName ) ;
621+ const exist = /** @type {HarmonySpecifierGuards= } */ (
622+ parser . getTagData ( rootName , harmonySpecifierGuardTag )
623+ ) ;
624+
625+ const mergedGuards =
626+ exist && exist . guards
627+ ? exist . guards . createChild ( )
628+ : new AppendOnlyStackedSet ( ) ;
629+
630+ for ( const memberKey of members ) mergedGuards . add ( memberKey ) ;
631+ parser . tagVariable ( rootName , harmonySpecifierGuardTag , {
632+ guards : mergedGuards
633+ } ) ;
634+ restoreFns . push ( ( ) => {
635+ parser . setVariable ( rootName , previous ) ;
636+ } ) ;
637+ }
638+
639+ return ( ) => {
640+ for ( const restore of restoreFns ) {
641+ restore ( ) ;
642+ }
643+ } ;
644+ } ;
645+
646+ const restore = applyGuards ( ) ;
647+ try {
648+ walk ( ) ;
649+ } finally {
650+ restore ( ) ;
651+ }
652+ } ;
653+
654+ parser . hooks . collectGuards . tap ( PLUGIN_NAME , ( expression ) => {
655+ if ( parser . scope . isAsmJs ) return ;
656+ /** @type {Guards } */
657+ const guards = new Map ( ) ;
658+ collect ( expression , guards , true ) ;
659+
660+ if ( guards . size === 0 ) return ;
661+ return ( walk ) => {
662+ withGuards ( guards , walk ) ;
663+ } ;
664+ } ) ;
405665 }
406666} ;
407667
668+ module . exports . harmonySpecifierGuardTag = harmonySpecifierGuardTag ;
408669module . exports . harmonySpecifierTag = harmonySpecifierTag ;
0 commit comments