From 53d0744c260caed321f55e573617059b7caad4bf Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Tue, 29 Sep 2026 15:26:13 +0800 Subject: [PATCH] fix(react): resolve heading names that share a substring with their receiver --- .changeset/full-name-receiver-substring.md | 5 ++ .../utils/__tests__/signature.test.mjs | 67 +++++++++++++++++++ .../react/src/jsx-ast/utils/signature.mjs | 37 ++++++++-- 3 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 .changeset/full-name-receiver-substring.md diff --git a/.changeset/full-name-receiver-substring.md b/.changeset/full-name-receiver-substring.md new file mode 100644 index 000000000..b35b84b11 --- /dev/null +++ b/.changeset/full-name-receiver-substring.md @@ -0,0 +1,5 @@ +--- +'@doc-kit/generator-react': patch +--- + +fix: resolve heading names that share a substring with their receiver diff --git a/packages/react/src/jsx-ast/utils/__tests__/signature.test.mjs b/packages/react/src/jsx-ast/utils/__tests__/signature.test.mjs index c6661df3d..0ae5670ee 100644 --- a/packages/react/src/jsx-ast/utils/__tests__/signature.test.mjs +++ b/packages/react/src/jsx-ast/utils/__tests__/signature.test.mjs @@ -372,4 +372,71 @@ describe('getFullName', () => { ); assert.strictEqual(result, 'fallbackValue'); }); + + it('skips occurrences of the name within the receiver', () => { + const result = getFullName({ + name: 'channel', + text: '`diagnostics_channel.channel(name)`', + }); + assert.strictEqual(result, 'diagnostics_channel.channel'); + }); + + it('skips occurrences of the name that are a prefix of the receiver', () => { + const result = getFullName({ + name: 'read', + text: '`readable.read([size])`', + }); + assert.strictEqual(result, 'readable.read'); + }); + + it('ignores parameters repeating the name', () => { + const result = getFullName({ + name: 'percentile', + text: '`histogram.percentile(percentile)`', + }); + assert.strictEqual(result, 'histogram.percentile'); + }); + + it('handles symbol-keyed methods', () => { + const result = getFullName({ + name: "[Symbol.for('nodejs.rejection')]", + text: "`emitter[Symbol.for('nodejs.rejection')](err, eventName[, ...args])`", + }); + assert.strictEqual(result, "emitter[Symbol.for('nodejs.rejection')]"); + }); + + it('keeps quoted names intact', () => { + const result = getFullName({ + name: 'console.log', + text: "Event: `'console.log'`", + }); + assert.strictEqual(result, 'console.log'); + }); + + it('does not strip "new" from within a name', () => { + const result = getFullName({ + name: 'newListener', + text: "Event: `'newListener'`", + }); + assert.strictEqual(result, 'newListener'); + }); + + it('does not strip "new" from within a dotted name', () => { + const result = getFullName({ + name: 'onnewtoken', + text: '`session.onnewtoken`', + }); + assert.strictEqual(result, 'session.onnewtoken'); + }); + + it('returns fallback when no occurrence terminates the name', () => { + const result = getFullName( + { + name: 'read', + text: '`readable`', + }, + 'fallback' + ); + assert.strictEqual(result, 'fallback'); + }); }); diff --git a/packages/react/src/jsx-ast/utils/signature.mjs b/packages/react/src/jsx-ast/utils/signature.mjs index d7032f326..4c991a2e4 100644 --- a/packages/react/src/jsx-ast/utils/signature.mjs +++ b/packages/react/src/jsx-ast/utils/signature.mjs @@ -86,12 +86,39 @@ export const getFullName = ({ name, text }, fallback = name) => { // Attempt to extract inline code from heading text const code = text.trim().match(/`([^`]+)`/)?.[1]; + if (!code?.includes(name)) { + return fallback; + } + + // Find the occurrence of `name` that denotes the documented entry: the one + // immediately followed by its parameter list, a closing quote, or the end + // of the code. Earlier occurrences are mere substrings of the receiver + // (e.g. `channel` within `diagnostics_channel.channel`, `read` within + // `readable.read`), and later ones can be parameters repeating the name. + let end = -1; + let index = code.indexOf(name); + + while (index !== -1) { + const next = code[index + name.length]; + + if (next === undefined || next === '(' || next === "'" || next === '"') { + end = index + name.length; + break; + } + + index = code.indexOf(name, index + 1); + } + // If inline code includes the name, return a clean version of it - return code?.includes(name) - ? code - .slice(0, code.indexOf(name) + name.length) // Truncate everything after the name. - .replace(/^["']|new\s*/g, '') // Strip quotes or "new" keyword - : fallback; + return end === -1 + ? fallback + : code + .slice(0, end) // Truncate everything after the name. + // Strip a leading quote and/or the "new" keyword. The latter requires + // following whitespace so names containing "new" (e.g. `newListener`) + // stay intact. + .replace(/^["']/, '') + .replace(/^new\s+/, ''); }; /**