@@ -463,7 +463,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
463463 'REX-L: the compiler is loaded only when a marker names the category': 6,
464464 'WS (#26220): ui workspace packages, `.tsx` modules, aliases and shims': 11,
465465 'IX (#26220): predicate 4 reads an `any` index signature; the entry may be `src/index.tsx`': 52,
466- 'WP (#26220): a deleted published package is retired whole, `<package>#*`': 50 ,
466+ 'WP (#26220): a deleted published package is retired whole, `<package>#*`': 55 ,
467467 'G6: a changeset that was ALREADY breaking at base is inherited': 1,
468468 'R15: a changeset RENAMED AND turned breaking in the same commit': 5,
469469 'G10: a PURE rename of an ALREADY-breaking stock changeset': 3,
@@ -4796,26 +4796,36 @@ function verifyWholePackageRetired(pkg, { base, head, cwd, packagesAt, kit }) {
47964796
47974797/**
47984798 * Predicate 2 for a whole package: the first metadata surface OUTSIDE `pkgDir` whose
4799- * code imports `pkg` (its root or a subpath) -- `from '…'`, a bare `import '…'`,
4800- * `import('…')` or `require('…')` -- or null. Read with comment spans blanked, as
4801- * `metadataSurfaceReference` reads: a docblock naming the package reaches nothing.
4799+ * CODE names `pkg`, or null -- D9's "no metadata surface references it in code",
4800+ * read as `metadataSurfaceReference` reads an export: comment spans blanked, string
4801+ * literals left standing. A package name can sit in code only inside a string, so
4802+ * any occurrence counts -- a plugin id or package reference held as a VALUE as much
4803+ * as an import. An import specifier (`from '…'`, a bare `import '…'`, `import('…')`,
4804+ * `require('…')`, by the root or a subpath) is the case reported as `import`; every
4805+ * other occurrence is a `mention`. A name that merely starts with `pkg`
4806+ * (`plugin-xy` for `plugin-x`) is not it.
48024807 *
48034808 * @param {string} pkg
48044809 * @param {string} pkgDir the package's directory, no trailing slash
48054810 * @param {Map<string, string>} surfaceTexts path -> text of every metadata surface
4806- * @returns {{ hitPath: string, line: string } | null}
4811+ * @returns {{ kind: 'import'|'mention', hitPath: string, line: string } | null}
48074812 */
4808- export function packageImporter (pkg, pkgDir, surfaceTexts) {
4813+ export function packageReference (pkg, pkgDir, surfaceTexts) {
48094814 const name = pkg.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
48104815 const spec = new RegExp(`(?:\\bfrom\\s*|\\bimport\\s*\\(\\s*|\\brequire\\s*\\(\\s*|^\\s*import\\s+)(['"])${name}(?:/[^'"\\n]*)?\\1`, 'm');
4816+ const mention = new RegExp(`(?<![\\w@./-])${name}(?![\\w.-])`);
4817+ // `maskComments` blanks in place, so an offset in `code` is the same offset in the file.
4818+ const lineAt = (raw, code, at) => {
4819+ const end = code.indexOf('\n', at);
4820+ return raw.slice(code.lastIndexOf('\n', at) + 1, end < 0 ? undefined : end).trim();
4821+ };
48114822 for (const [hitPath, rawText] of surfaceTexts) {
48124823 if (hitPath.startsWith(`${pkgDir}/`) || !rawText.includes(pkg)) continue;
48134824 const code = maskComments(rawText);
4814- const m = spec.exec(code);
4815- if (!m) continue;
4816- // `maskComments` blanks in place, so an offset in `code` is the same offset in the file.
4817- const end = code.indexOf('\n', m.index);
4818- return { hitPath, line: rawText.slice(code.lastIndexOf('\n', m.index) + 1, end < 0 ? undefined : end).trim() };
4825+ const imp = spec.exec(code);
4826+ if (imp) return { kind: 'import', hitPath, line: lineAt(rawText, code, imp.index) };
4827+ const m = mention.exec(code);
4828+ if (m) return { kind: 'mention', hitPath, line: lineAt(rawText, code, m.index) };
48194829 }
48204830 return null;
48214831}
@@ -5127,7 +5137,7 @@ export function verifyRuntimeExportRetired(refs, { base, head, cwd, body, packag
51275137 const t = `\`runtime-export-retired ${op.ref}\` [predicate 2: no-metadata-surface]`;
51285138 if (op.whole) {
51295139 // The same question, asked of the PACKAGE: none of its files is a metadata
5130- // surface, and no metadata surface outside it imports it.
5140+ // surface, and no metadata surface outside it names it in code .
51315141 let own = [];
51325142 try { own = git(['ls-tree', '-r', '--name-only', base, '--', `${r.pkgDir}/`], cwd).split('\n').map((s) => s.trim()).filter(Boolean); } catch { /* listed below as none */ }
51335143 const site = own.find((f) => metadataSurfaceKind(f) !== null);
@@ -5140,11 +5150,12 @@ export function verifyRuntimeExportRetired(refs, { base, head, cwd, body, packag
51405150 continue;
51415151 }
51425152 metadataTexts ??= showManyOrNull(base, surfacePaths, cwd);
5143- const importer = packageImporter (op.pkg, r.pkgDir, metadataTexts);
5144- if (importer ) {
5153+ const hit = packageReference (op.pkg, r.pkgDir, metadataTexts);
5154+ if (hit ) {
51455155 problems.push(
5146- `${t} is false: ${importer.hitPath} (${metadataSurfaceKind(importer.hitPath)}) IMPORTS \`${op.pkg}\`:\n` +
5147- ` ${importer.line.slice(0, 140)}\n` +
5156+ `${t} is false: ${hit.hitPath} (${metadataSurfaceKind(hit.hitPath)}) ` +
5157+ `${hit.kind === 'import' ? 'IMPORTS' : 'names'} \`${op.pkg}\` IN CODE:\n` +
5158+ ` ${hit.line.slice(0, 140)}\n` +
51485159 ' A package a metadata surface reaches HAS a metadata surface (comment spans are already excluded).',
51495160 );
51505161 continue;
@@ -8217,6 +8228,10 @@ function selfTest() {
82178228 red('WP-R10 a metadata surface OUTSIDE the package imports it at the merge base', wp({
82188229 base: { 'packages/spec/src/ui/widget.zod.ts': `import { Widget } from '${WP_PKG}';\nexport const W = Widget;\n` },
82198230 }), [P2, /packages\/spec\/src\/ui\/widget\.zod\.ts \(a Zod schema\) IMPORTS `@object-ui\/plugin-x`/]);
8231+ red('WP-R14 a metadata surface OUTSIDE the package names it as a string VALUE (no import) at the merge base', wp({
8232+ base: { 'packages/platform-objects/src/ui/widget-host.object.ts': `export const WidgetHost = { name: 'widget_host', plugin: '${WP_PKG}' };\n` },
8233+ }), [P2, /packages\/platform-objects\/src\/ui\/widget-host\.object\.ts \(an object definition\) names `@object-ui\/plugin-x` IN CODE/,
8234+ /plugin: '@object-ui\/plugin-x'/]);
82208235 red('WP-R11 no FROM line names the package', wp({ cs: WP_CS('Nothing else to do.') }),
82218236 [/the named operand `@object-ui\/plugin-x#\*` has no FROM side/, /- FROM `@object-ui\/plugin-x` TO/]);
82228237 red('WP-R12 one FROM line per EXPORT instead of the package line', wp({ cs: WP_CS('- FROM `Widget` TO nothing: delete the import.') }),
@@ -8236,10 +8251,17 @@ function selfTest() {
82368251 ['packages/spec/src/b.zod.ts', "import { X } from '@object-ui/plugin-xy';\n"],
82378252 ['ui/packages/plugin-x/src/c.zod.ts', `import '${WP_PKG}';\n`],
82388253 ]);
8239- assert(packageImporter(WP_PKG, 'ui/packages/plugin-x', texts)?.hitPath === 'packages/spec/src/a.zod.ts',
8240- 'WP-U3: a SUBPATH import is an import of the package; the package\'s own files are skipped');
8241- assert(packageImporter(WP_PKG, 'ui/packages/plugin-x', new Map([...texts].slice(1))) === null,
8254+ const u3 = packageReference(WP_PKG, 'ui/packages/plugin-x', texts);
8255+ assert(u3?.hitPath === 'packages/spec/src/a.zod.ts' && u3.kind === 'import',
8256+ `WP-U3: a SUBPATH import is an import of the package; the package's own files are skipped (${JSON.stringify(u3)})`);
8257+ assert(packageReference(WP_PKG, 'ui/packages/plugin-x', new Map([...texts].slice(1))) === null,
82428258 'WP-U4: a package whose name merely STARTS with the name (`plugin-xy`) is not the package');
8259+ const u5 = packageReference(WP_PKG, 'ui/packages/plugin-x', new Map([
8260+ ['packages/spec/src/c.zod.ts', `// '${WP_PKG}' in a comment\nexport const C = { from: 'x' };\n`],
8261+ ['packages/spec/src/d.zod.ts', `export const D = { plugin: '${WP_PKG}' };\n`],
8262+ ]));
8263+ assert(u5?.kind === 'mention' && u5.hitPath === 'packages/spec/src/d.zod.ts' && u5.line === `export const D = { plugin: '${WP_PKG}' };`,
8264+ `WP-U5: a string VALUE naming the package is a reference in code, reported with its line; a comment is not (${JSON.stringify(u5)})`);
82438265 }
82448266
82458267 // ---- G6: a changeset that was ALREADY breaking at base is inherited -------
0 commit comments