Skip to content

Commit a58c0d2

Browse files
committed
fix(legacy-json): don't mutate the ast
1 parent cc901ec commit a58c0d2

5 files changed

Lines changed: 53 additions & 59 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@node-core/doc-kit': patch
3+
---
4+
5+
Avoids directly mutating the AST in `legacy-json`, as to ensure future generators do not run with a input different than they expect.

src/generators/legacy-json/types.d.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@ import { ListItem } from '@types/mdast';
22
import { MetadataEntry } from '../metadata/types';
33

44
/**
5-
* Represents an entry in a hierarchical structure, extending from MetadataEntry.
6-
* It includes children entries organized in a hierarchy.
5+
* A node in the entry hierarchy.
76
*/
8-
export interface HierarchizedEntry extends MetadataEntry {
7+
export interface HierarchizedEntry {
98
/**
10-
* List of child entries that are part of this entry's hierarchy.
9+
* The metadata entry this node wraps.
1110
*/
12-
hierarchyChildren: MetadataEntry[];
11+
entry: MetadataEntry;
12+
13+
/**
14+
* Child nodes nested under this entry, based on heading depth.
15+
*/
16+
children: HierarchizedEntry[];
1317
}
1418

1519
/**

src/generators/legacy-json/utils/__tests__/buildHierarchy.test.mjs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ import { findParent, buildHierarchy } from '../buildHierarchy.mjs';
55

66
describe('findParent', () => {
77
it('finds parent with lower depth', () => {
8-
const entries = [{ heading: { depth: 1 } }, { heading: { depth: 2 } }];
9-
const parent = findParent(entries[1], entries, 0);
10-
assert.equal(parent, entries[0]);
8+
const nodes = [
9+
{ entry: { heading: { depth: 1 } }, children: [] },
10+
{ entry: { heading: { depth: 2 } }, children: [] },
11+
];
12+
const parent = findParent(nodes[1].entry, nodes, 0);
13+
assert.equal(parent, nodes[0]);
1114
});
1215

1316
it('throws when no parent exists', () => {
14-
const entries = [{ heading: { depth: 2 } }];
15-
assert.throws(() => findParent(entries[0], entries, -1));
17+
const nodes = [{ entry: { heading: { depth: 2 } }, children: [] }];
18+
assert.throws(() => findParent(nodes[0].entry, nodes, -1));
1619
});
1720
});
1821

@@ -25,15 +28,17 @@ describe('buildHierarchy', () => {
2528
const entries = [{ heading: { depth: 1 } }, { heading: { depth: 1 } }];
2629
const result = buildHierarchy(entries);
2730
assert.equal(result.length, 2);
31+
assert.equal(result[0].entry, entries[0]);
32+
assert.equal(result[1].entry, entries[1]);
2833
});
2934

3035
it('nests children under parents', () => {
3136
const entries = [{ heading: { depth: 1 } }, { heading: { depth: 2 } }];
3237
const result = buildHierarchy(entries);
3338

3439
assert.equal(result.length, 1);
35-
assert.equal(result[0].hierarchyChildren.length, 1);
36-
assert.equal(result[0].hierarchyChildren[0], entries[1]);
40+
assert.equal(result[0].children.length, 1);
41+
assert.equal(result[0].children[0].entry, entries[1]);
3742
});
3843

3944
it('handles multiple levels', () => {
@@ -45,6 +50,6 @@ describe('buildHierarchy', () => {
4550
const result = buildHierarchy(entries);
4651

4752
assert.equal(result.length, 1);
48-
assert.equal(result[0].hierarchyChildren[0].hierarchyChildren.length, 1);
53+
assert.equal(result[0].children[0].children.length, 1);
4954
});
5055
});

src/generators/legacy-json/utils/buildHierarchy.mjs

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,28 @@
11
/**
2-
* Recursively finds the most suitable parent entry for a given `entry` based on heading depth.
2+
* Recursively finds the most suitable parent node for a given `entry` based on heading depth.
33
*
44
* @param {import('../../metadata/types').MetadataEntry} entry
5-
* @param {import('../../metadata/types').MetadataEntry[]} entries
5+
* @param {Array<import('../types.d.ts').HierarchizedEntry>} nodes
66
* @param {number} startIdx
77
* @returns {import('../types.d.ts').HierarchizedEntry}
88
*/
9-
export function findParent(entry, entries, startIdx) {
9+
export function findParent(entry, nodes, startIdx) {
1010
// Base case: if we're at the beginning of the list, no valid parent exists.
1111
if (startIdx < 0) {
1212
throw new Error(
1313
`Cannot find a suitable parent for entry at index ${startIdx + 1}`
1414
);
1515
}
1616

17-
const candidateParent = entries[startIdx];
18-
const candidateDepth = candidateParent.heading.depth;
17+
const candidateParent = nodes[startIdx];
1918

2019
// If we find a suitable parent, return it.
21-
if (candidateDepth < entry.heading.depth) {
22-
candidateParent.hierarchyChildren ??= [];
20+
if (candidateParent.entry.heading.depth < entry.heading.depth) {
2321
return candidateParent;
2422
}
2523

2624
// Recurse upwards to find a suitable parent.
27-
return findParent(entry, entries, startIdx - 1);
25+
return findParent(entry, nodes, startIdx - 1);
2826
}
2927

3028
/**
@@ -37,41 +35,31 @@ export function findParent(entry, entries, startIdx) {
3735
*
3836
* If depth <= 1, it's a top-level element (aka a root).
3937
*
40-
* If it's depth is greater than the previous entry's depth, it's a child of
41-
* the previous entry. Otherwise (if it's less than or equal to the previous
42-
* entry's depth), we need to find the entry that it was the greater than. We
43-
* can do this by just looping through entries in reverse starting at the
44-
* current index - 1.
38+
* Otherwise, its parent is the nearest earlier entry with a lower depth,
39+
* found by looping through entries in reverse starting at the current
40+
* index - 1.
4541
*
4642
* @param {Array<import('../../metadata/types').MetadataEntry>} entries
4743
* @returns {Array<import('../types.d.ts').HierarchizedEntry>}
4844
*/
4945
export function buildHierarchy(entries) {
5046
const roots = [];
5147

48+
// Wrapper nodes, index-aligned with `entries`.
49+
const nodes = entries.map(entry => ({ entry, children: [] }));
50+
5251
// Main loop to construct the hierarchy.
53-
for (let i = 0; i < entries.length; i++) {
54-
const entry = entries[i];
55-
const currentDepth = entry.heading.depth;
52+
for (let i = 0; i < nodes.length; i++) {
53+
const node = nodes[i];
5654

5755
// Top-level entries are added directly to roots.
58-
if (currentDepth <= 1) {
59-
roots.push(entry);
56+
if (node.entry.heading.depth <= 1) {
57+
roots.push(node);
6058
continue;
6159
}
6260

6361
// For non-root entries, find the appropriate parent.
64-
const previousEntry = entries[i - 1];
65-
const previousDepth = previousEntry.heading.depth;
66-
67-
if (currentDepth > previousDepth) {
68-
previousEntry.hierarchyChildren ??= [];
69-
previousEntry.hierarchyChildren.push(entry);
70-
} else {
71-
// Use recursive helper to find the nearest valid parent.
72-
const parent = findParent(entry, entries, i - 2);
73-
parent.hierarchyChildren.push(entry);
74-
}
62+
findParent(node.entry, nodes, i - 1).children.push(node);
7563
}
7664

7765
return roots;

src/generators/legacy-json/utils/buildSection.mjs

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ export const promoteMiscChildren = (section, parent) => {
3232
*/
3333
export const createSectionBuilder = () => {
3434
/**
35-
* Creates metadata from a hierarchized entry.
36-
* @param {import('../types.d.ts').HierarchizedEntry} entry - The entry to create metadata from.
35+
* Creates metadata from a metadata entry.
36+
* @param {import('../../metadata/types').MetadataEntry} entry - The entry to create metadata from.
3737
* @returns {import('../types.d.ts').Meta | undefined} The created metadata, or undefined if all fields are empty.
3838
*/
3939
const createMeta = ({
@@ -73,7 +73,7 @@ export const createSectionBuilder = () => {
7373

7474
/**
7575
* Creates a section from an entry and its heading.
76-
* @param {import('../types.d.ts').HierarchizedEntry} entry - The AST entry.
76+
* @param {import('../../metadata/types').MetadataEntry} entry - The AST entry.
7777
* @param {import('../../metadata/types').HeadingNode} head - The head node of the entry.
7878
* @returns {import('../types.d.ts').Section} The created section.
7979
*/
@@ -98,7 +98,7 @@ export const createSectionBuilder = () => {
9898
* Parses stability metadata and adds it to the section.
9999
* @param {import('../types.d.ts').Section} section - The section to update.
100100
* @param {Array} nodes - The remaining AST nodes.
101-
* @param {import('../types.d.ts').HierarchizedEntry} entry - The entry providing stability information.
101+
* @param {import('../../metadata/types').MetadataEntry} entry - The entry providing stability information.
102102
*/
103103
const parseStability = (section, nodes, { stability, content }) => {
104104
if (stability) {
@@ -161,26 +161,18 @@ export const createSectionBuilder = () => {
161161
};
162162

163163
/**
164-
* Processes children of a given entry and updates the section.
165-
* @param {import('../types.d.ts').HierarchizedEntry} entry - The current entry.
166-
* @param {import('../types.d.ts').Section} section - The current section.
167-
*/
168-
const handleChildren = ({ hierarchyChildren }, section) =>
169-
hierarchyChildren?.forEach(child => handleEntry(child, section));
170-
171-
/**
172-
* Handles an entry and updates the parent section.
173-
* @param {import('../types.d.ts').HierarchizedEntry} entry - The entry to process.
164+
* Handles a hierarchy node and updates the parent section.
165+
* @param {import('../types.d.ts').HierarchizedEntry} node - The hierarchy node to process.
174166
* @param {import('../types.d.ts').Section} parent - The parent section.
175167
*/
176-
const handleEntry = (entry, parent) => {
168+
const handleEntry = ({ entry, children }, parent) => {
177169
const [headingNode, ...nodes] = entry.content.children;
178170
const section = createSection(entry, headingNode);
179171

180172
parseStability(section, nodes, entry);
181173
parseList(section, nodes);
182174
addDescription(section, nodes);
183-
handleChildren(entry, section);
175+
children.forEach(child => handleEntry(child, section));
184176
addAdditionalMetadata(section, parent, headingNode);
185177
addToParent(section, parent);
186178
promoteMiscChildren(section, parent);
@@ -200,7 +192,7 @@ export const createSectionBuilder = () => {
200192
source: `doc/api/${head.api}.md`,
201193
};
202194

203-
buildHierarchy(entries).forEach(entry => handleEntry(entry, rootModule));
195+
buildHierarchy(entries).forEach(node => handleEntry(node, rootModule));
204196

205197
return rootModule;
206198
};

0 commit comments

Comments
 (0)