@@ -36,9 +36,16 @@ const registryKind: PlatformExecutionKindOf = (command) =>
3636
3737type RouteDispatch = Readonly < { route : string ; dispatched : string } > ;
3838
39+ /**
40+ * One dispatch expression, identified by source position. Attribution subtracts
41+ * occurrences: two dispatches of the same command are two sites, so a stray one
42+ * cannot be absorbed by a routed one that happens to name the same command.
43+ */
44+ type DispatchSite = Readonly < { command : string ; offset : number } > ;
45+
3946type RouteScan = Readonly < {
4047 dispatches : readonly RouteDispatch [ ] ;
41- /** Dispatch literals no `command === '<name>'` route could claim. */
48+ /** Commands dispatched at a site no `command === '<name>'` route could claim. */
4249 unattributed : readonly string [ ] ;
4350} > ;
4451
@@ -88,10 +95,15 @@ function routedCommandOf(test: unknown): string | undefined {
8895}
8996
9097/** `command: 'x'`, `command: INTERNAL_COMMANDS.x`, `command: PUBLIC_COMMANDS.x`. */
91- function dispatchedCommandOf ( node : AstNode ) : string | undefined {
98+ function dispatchSiteOf ( node : AstNode ) : DispatchSite | undefined {
9299 if ( node . type !== 'Property' || node [ 'computed' ] === true ) return undefined ;
93100 if ( identifierName ( node [ 'key' ] ) !== 'command' ) return undefined ;
94- return dispatchTargetOf ( node [ 'value' ] ) ;
101+ const command = dispatchTargetOf ( node [ 'value' ] ) ;
102+ if ( command === undefined ) return undefined ;
103+ // A node without a source position cannot be matched against an attributed
104+ // site, so -1 keeps it distinct from every real offset and it stays unclaimed.
105+ const start = node [ 'start' ] ;
106+ return { command, offset : typeof start === 'number' ? start : - 1 } ;
95107}
96108
97109function dispatchTargetOf ( value : unknown ) : string | undefined {
@@ -115,57 +127,60 @@ function localFunctionsByName(program: unknown): Map<string, AstNode> {
115127 return functions ;
116128}
117129
118- /** Dispatch literals reachable from `scope`, following calls to same-module functions. */
130+ /** Dispatch sites reachable from `scope`, following calls to same-module functions. */
119131function reachableDispatches (
120132 scope : unknown ,
121133 functions : ReadonlyMap < string , AstNode > ,
122134 visited : Set < string > ,
123- ) : Set < string > {
124- const found = new Set < string > ( ) ;
135+ ) : DispatchSite [ ] {
136+ const found : DispatchSite [ ] = [ ] ;
125137 const calls : string [ ] = [ ] ;
126138 walk ( scope , ( node ) => {
127- const dispatched = dispatchedCommandOf ( node ) ;
128- if ( dispatched !== undefined ) found . add ( dispatched ) ;
139+ const site = dispatchSiteOf ( node ) ;
140+ if ( site !== undefined ) found . push ( site ) ;
129141 if ( node . type !== 'CallExpression' ) return ;
130142 const callee = identifierName ( node [ 'callee' ] ) ;
131143 if ( callee !== undefined && functions . has ( callee ) ) calls . push ( callee ) ;
132144 } ) ;
133145 for ( const name of calls ) {
134146 if ( visited . has ( name ) ) continue ;
135147 visited . add ( name ) ;
136- for ( const dispatched of reachableDispatches ( functions . get ( name ) , functions , visited ) ) {
137- found . add ( dispatched ) ;
138- }
148+ found . push ( ...reachableDispatches ( functions . get ( name ) , functions , visited ) ) ;
139149 }
140150 return found ;
141151}
142152
143153function scanCliRouteDispatches ( sourceText : string ) : RouteScan {
144154 const program = parseSync ( 'cli.ts' , sourceText ) . program ;
145155 const functions = localFunctionsByName ( program ) ;
156+ const edges = new Set < string > ( ) ;
146157 const dispatches : RouteDispatch [ ] = [ ] ;
147- const attributed = new Set < string > ( ) ;
158+ const attributedOffsets = new Set < number > ( ) ;
148159
149160 walk ( program , ( node ) => {
150161 if ( node . type !== 'IfStatement' ) return ;
151162 const route = routedCommandOf ( node [ 'test' ] ) ;
152163 if ( route === undefined ) return ;
153- for ( const dispatched of reachableDispatches ( node [ 'consequent' ] , functions , new Set ( ) ) ) {
154- dispatches . push ( { route, dispatched } ) ;
155- attributed . add ( dispatched ) ;
164+ for ( const site of reachableDispatches ( node [ 'consequent' ] , functions , new Set ( ) ) ) {
165+ attributedOffsets . add ( site . offset ) ;
166+ const edge = `${ route } ${ site . command } ` ;
167+ if ( edges . has ( edge ) ) continue ;
168+ edges . add ( edge ) ;
169+ dispatches . push ( { route, dispatched : site . command } ) ;
156170 }
157171 } ) ;
158172
159- const all = new Set < string > ( ) ;
173+ const allSites : DispatchSite [ ] = [ ] ;
160174 walk ( program , ( node ) => {
161- const dispatched = dispatchedCommandOf ( node ) ;
162- if ( dispatched !== undefined ) all . add ( dispatched ) ;
175+ const site = dispatchSiteOf ( node ) ;
176+ if ( site !== undefined ) allSites . push ( site ) ;
163177 } ) ;
164178
165- return {
166- dispatches,
167- unattributed : [ ...all ] . filter ( ( command ) => ! attributed . has ( command ) ) . sort ( ) ,
168- } ;
179+ const unclaimed = allSites
180+ . filter ( ( { offset } ) => ! attributedOffsets . has ( offset ) )
181+ . map ( ( { command } ) => command ) ;
182+
183+ return { dispatches, unattributed : [ ...new Set ( unclaimed ) ] . sort ( ) } ;
169184}
170185
171186function dominanceFailures ( scan : RouteScan , kindOf : PlatformExecutionKindOf ) : string [ ] {
@@ -215,4 +230,24 @@ describe('platform-execution coherence across CLI route delegation', () => {
215230 ` ;
216231 expect ( scanCliRouteDispatches ( planted ) . unattributed ) . toEqual ( [ 'runtime' ] ) ;
217232 } ) ;
233+
234+ test ( 'planted red: a stray dispatch sharing a routed target name is still reported' , ( ) => {
235+ const planted = `
236+ async function runCli(argv, deps) {
237+ if (command === 'react-devtools') {
238+ await runReactDevtoolsCli(ctx, deps);
239+ return;
240+ }
241+ await deps.sendToDaemon({ command: INTERNAL_COMMANDS.runtime, positionals: ['stray'] });
242+ }
243+ async function runReactDevtoolsCli(ctx, deps) {
244+ await deps.sendToDaemon({ command: INTERNAL_COMMANDS.runtime, positionals: [] });
245+ }
246+ ` ;
247+ const scan = scanCliRouteDispatches ( planted ) ;
248+
249+ // Both halves matter: the routed occurrence is claimed, the stray one is not.
250+ expect ( scan . dispatches ) . toEqual ( [ { route : 'react-devtools' , dispatched : 'runtime' } ] ) ;
251+ expect ( scan . unattributed ) . toEqual ( [ 'runtime' ] ) ;
252+ } ) ;
218253} ) ;
0 commit comments