|
117 | 117 | // count(...)` on a locally-bound variable is pseudo-code and is never read. |
118 | 118 | // Anything whose type is `any`, or which carries an index signature, is not |
119 | 119 | // reported -- absence of a property there is not evidence. |
| 120 | +// |
| 121 | +// That fence is a CHARACTER CLASS, and #9610 measured what happens when it is |
| 122 | +// spelled as a consuming alternation instead of a zero-width assertion: the |
| 123 | +// receiver in `kernel.use(SomePlugin.configure(…))` became unreachable, because |
| 124 | +// the outer call had already eaten the `(` in front of it. See extractMemberCalls. |
120 | 125 |
|
121 | 126 | import { existsSync, readFileSync, readdirSync } from 'node:fs'; |
122 | 127 | import { join, posix, resolve } from 'node:path'; |
@@ -337,14 +342,31 @@ export function extractMemberCalls(markdown, localNames) { |
337 | 342 | for (const { n, text } of fence.lines) { |
338 | 343 | // Skip the import statements themselves and single-line comments. |
339 | 344 | if (/^\s*(import\b|\/\/|\*|\/\*)/.test(text)) continue; |
340 | | - const rx = /(^|[^\w$.'"`])([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)\s*\(/g; |
| 345 | + // ⛔ The leading boundary is ASSERTED, never consumed (#9610). The obvious |
| 346 | + // spelling -- `(^|[^\w$.'"`])` -- eats the character in front of the receiver, |
| 347 | + // and `rx` is global, so a receiver beginning at the very next character after |
| 348 | + // a previous match has no boundary left to match against. A match always ends |
| 349 | + // at its own `(`, which makes the swallowed position exactly `outer(Inner.m(` |
| 350 | + // -- and `kernel.use(SomePlugin.configure({…}))` is the house spelling of every |
| 351 | + // README this gate was built for, so the blind spot was the NORMAL position, |
| 352 | + // not a corner. Measured on the published regex, one space apart: |
| 353 | + // |
| 354 | + // kernel.use(CacheServicePlugin.configure({…})) -> extracted: kernel.use |
| 355 | + // kernel.use( CacheServicePlugin.configure({…})) -> extracted: both |
| 356 | + // |
| 357 | + // A negative lookbehind is zero-width, so nothing is consumed and the `^` arm |
| 358 | + // folds in (a negative lookbehind is satisfied at position 0). The character |
| 359 | + // class is byte-for-byte the old one, so the fence is unchanged: `a.b.c(` and |
| 360 | + // `'str'.trim(` stay out -- now in the nested position too, which is the only |
| 361 | + // position this change newly reaches. |
| 362 | + const rx = /(?<![\w$.'"`])([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)\s*\(/g; |
341 | 363 | let m; |
342 | 364 | while ((m = rx.exec(text)) !== null) { |
343 | | - if (!wanted.has(m[2])) continue; |
344 | | - const key = `${m[2]}.${m[3]}`; |
| 365 | + if (!wanted.has(m[1])) continue; |
| 366 | + const key = `${m[1]}.${m[2]}`; |
345 | 367 | if (seen.has(key)) continue; |
346 | 368 | seen.add(key); |
347 | | - out.push({ line: n, object: m[2], member: m[3] }); |
| 369 | + out.push({ line: n, object: m[1], member: m[2] }); |
348 | 370 | } |
349 | 371 | } |
350 | 372 | } |
@@ -954,6 +976,44 @@ function selfTest() { |
954 | 976 | [], |
955 | 977 | ); |
956 | 978 |
|
| 979 | + // -- the adversarial position (#9610), which the fixture above cannot reach ---- |
| 980 | + // Every case above puts the wanted receiver where no earlier match on the line |
| 981 | + // has consumed anything in front of it. That is why a gate written with |
| 982 | + // self-tests in BOTH directions still shipped blind to the shape below: the |
| 983 | + // receiver starts at the character immediately after a DISCARDED `X.y(` match, |
| 984 | + // with nothing separating them. It is also the house spelling of the six READMEs |
| 985 | + // this gate exists for, so it is the likeliest wrong rewrite of any of them. |
| 986 | + const nestedReceiver = [ |
| 987 | + '```typescript', |
| 988 | + "import { CacheServicePlugin } from '@objectstack/service-cache';", |
| 989 | + 'await kernel.use(CacheServicePlugin.configure({ adapter: "memory" }));', |
| 990 | + '```', |
| 991 | + ].join('\n'); |
| 992 | + eq( |
| 993 | + 'extractMemberCalls — a receiver directly inside a discarded call is still read', |
| 994 | + extractMemberCalls(nestedReceiver, ['CacheServicePlugin']).map((c) => `${c.object}.${c.member}`), |
| 995 | + ['CacheServicePlugin.configure'], |
| 996 | + ); |
| 997 | + |
| 998 | + // The other direction, in that SAME position: reaching it must not widen the |
| 999 | + // fence. Property access, all three quote styles, and the CORRECT `new X(` |
| 1000 | + // spelling stay silent when nested exactly as above. |
| 1001 | + const nestedRejected = [ |
| 1002 | + '```typescript', |
| 1003 | + "import { CacheServicePlugin } from '@objectstack/service-cache';", |
| 1004 | + 'await kernel.use(wrapper.CacheServicePlugin.configure({}));', |
| 1005 | + "console.log('CacheServicePlugin.configure(');", |
| 1006 | + 'console.log("CacheServicePlugin.configure(");', |
| 1007 | + 'console.log(`CacheServicePlugin.configure(`);', |
| 1008 | + 'await kernel.use(new CacheServicePlugin({ adapter: "memory" }));', |
| 1009 | + '```', |
| 1010 | + ].join('\n'); |
| 1011 | + eq( |
| 1012 | + 'extractMemberCalls — the nested position does not widen the fence', |
| 1013 | + extractMemberCalls(nestedRejected, ['CacheServicePlugin']), |
| 1014 | + [], |
| 1015 | + ); |
| 1016 | + |
957 | 1017 | // -- specifier splitting ------------------------------------------------------ |
958 | 1018 | eq('splitSpecifier — scoped root', splitSpecifier('@objectstack/spec'), { |
959 | 1019 | name: '@objectstack/spec', |
@@ -1065,6 +1125,25 @@ function selfTest() { |
1065 | 1125 | ['@objectstack/kernel|packages/kernel/README.md|member|@objectstack/kernel|Kernel.configure'], |
1066 | 1126 | ); |
1067 | 1127 |
|
| 1128 | + // ...and the same fabricated static written the way plugin registration is |
| 1129 | + // actually written: nested inside another call, no separator (#9610). This ran |
| 1130 | + // GREEN end to end on the real tree before the boundary became zero-width. |
| 1131 | + const fabricatedStaticNested = { |
| 1132 | + pkg: '@objectstack/kernel', |
| 1133 | + file: 'packages/kernel/README.md', |
| 1134 | + text: [ |
| 1135 | + '```typescript', |
| 1136 | + "import { Kernel } from '@objectstack/kernel';", |
| 1137 | + 'await app.use(Kernel.configure({}));', |
| 1138 | + '```', |
| 1139 | + ].join('\n'), |
| 1140 | + }; |
| 1141 | + eq( |
| 1142 | + 'analyzeDocument — a fabricated static nested inside another call is reported', |
| 1143 | + analyzeDocument(fabricatedStaticNested, resolveFake).map((f) => f.id), |
| 1144 | + ['@objectstack/kernel|packages/kernel/README.md|member|@objectstack/kernel|Kernel.configure'], |
| 1145 | + ); |
| 1146 | + |
1068 | 1147 | // Undeclared subpath: the packaged surface says something the source does not. |
1069 | 1148 | const badSubpath = { |
1070 | 1149 | pkg: '@objectstack/service-analytics', |
|
0 commit comments