Skip to content

Commit 9f96c57

Browse files
committed
feat: add support for import tags
1 parent 2c6d363 commit 9f96c57

6 files changed

Lines changed: 195 additions & 51 deletions

File tree

src/parser.ts

Lines changed: 145 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
findPluginByParser,
88
isDefaultTag,
99
} from "./utils.js";
10-
import { DESCRIPTION, PARAM, RETURNS, EXAMPLE } from "./tags.js";
10+
import { DESCRIPTION, PARAM, RETURNS, EXAMPLE, IMPORT } from "./tags.js";
1111
import {
1212
TAGS_DESCRIPTION_NEEDED,
1313
TAGS_GROUP_HEAD,
@@ -255,61 +255,118 @@ function sortTags(
255255
): Spec[] {
256256
let canGroupNextTags = false;
257257
let shouldSortAgain = false;
258+
const importDetailsBySource: { [tag: string]: ImportDetails[] } = {};
259+
260+
const tagGroups = tags.reduce<Spec[][]>((tagGroups, cur) => {
261+
if (
262+
tagGroups.length === 0 ||
263+
(TAGS_GROUP_HEAD.includes(cur.tag) && canGroupNextTags)
264+
) {
265+
canGroupNextTags = false;
266+
tagGroups.push([]);
267+
}
258268

259-
tags = tags
260-
.reduce<Spec[][]>((tagGroups, cur) => {
261-
if (
262-
tagGroups.length === 0 ||
263-
(TAGS_GROUP_HEAD.includes(cur.tag) && canGroupNextTags)
264-
) {
265-
canGroupNextTags = false;
266-
tagGroups.push([]);
267-
}
268-
if (TAGS_GROUP_CONDITION.includes(cur.tag)) {
269-
canGroupNextTags = true;
270-
}
271-
tagGroups[tagGroups.length - 1].push(cur);
272-
273-
return tagGroups;
274-
}, [])
275-
.flatMap((tagGroup, index, array) => {
276-
// sort tags within groups
277-
tagGroup.sort((a, b) => {
278-
if (
279-
paramsOrder &&
280-
paramsOrder.length > 1 &&
281-
a.tag === PARAM &&
282-
b.tag === PARAM
283-
) {
284-
const aIndex = paramsOrder.indexOf(a.name);
285-
const bIndex = paramsOrder.indexOf(b.name);
286-
if (aIndex > -1 && bIndex > -1) {
287-
//sort params
288-
return aIndex - bIndex;
289-
}
290-
return 0;
291-
}
292-
return (
293-
getTagOrderWeight(a.tag, options) - getTagOrderWeight(b.tag, options)
294-
);
295-
});
269+
if (TAGS_GROUP_CONDITION.includes(cur.tag)) {
270+
canGroupNextTags = true;
271+
}
296272

297-
// add an empty line between groups
298-
if (array.length - 1 !== index) {
299-
tagGroup.push(SPACE_TAG_DATA);
273+
if (cur.tag === IMPORT) {
274+
const importDetails = getImportDetails(cur);
275+
if (importDetails) {
276+
const existingImport = importDetailsBySource[importDetails.src];
277+
if (existingImport) {
278+
importDetailsBySource[importDetails.src].push(importDetails);
279+
// do not add duplicate import tags to tagGroups
280+
return tagGroups;
281+
}
282+
importDetailsBySource[importDetails.src] = [importDetails];
300283
}
284+
}
285+
286+
tagGroups[tagGroups.length - 1].push(cur);
287+
288+
return tagGroups;
289+
}, []);
290+
291+
// Merge the import details for a given src into a printable tag description
292+
Object.keys(importDetailsBySource).forEach((src) => {
293+
const importDetails = importDetailsBySource[src];
294+
// the first spec is the only one added to tagGroups
295+
const firstImpSpec = importDetails[0].spec;
296+
const { defaultImport, namedImports } = importDetails.reduce(
297+
(prev, curr) => {
298+
prev.namedImports.push(...curr.namedImports);
299+
// NB: the last default import encountered will be the one used
300+
if (curr.defaultImport) prev.defaultImport = curr.defaultImport;
301+
return prev;
302+
},
303+
{ namedImports: [], defaultImport: undefined } as Pick<
304+
ImportDetails,
305+
"defaultImport" | "namedImports"
306+
>,
307+
);
308+
// sort the import details
309+
namedImports.sort((a, b) =>
310+
(a.alias ?? a.name).localeCompare(b.alias ?? b.name),
311+
);
312+
313+
// write the merged import details to the spec description
314+
const importClauses = [];
315+
if (defaultImport) importClauses.push(defaultImport);
316+
if (namedImports.length > 0) {
317+
const makeMultiLine = namedImports.length > 1;
318+
const typeString = namedImports
319+
.map((t) => {
320+
const val = t.alias ? `${t.name} as ${t.alias}` : `${t.name}`;
321+
return makeMultiLine ? ` ${val}` : val;
322+
})
323+
.join(",\n");
324+
const namedImportClause = makeMultiLine
325+
? `{\n${typeString}\n}`
326+
: `{${typeString}}`;
327+
importClauses.push(namedImportClause);
328+
}
329+
firstImpSpec.description = `${importClauses.join(", ")} from "${src}"`;
330+
});
301331

332+
tags = tagGroups.flatMap((tagGroup, index, array) => {
333+
// sort tags within groups
334+
tagGroup.sort((a, b) => {
302335
if (
303-
index > 0 &&
304-
tagGroup[0]?.tag &&
305-
!TAGS_GROUP_HEAD.includes(tagGroup[0].tag)
336+
paramsOrder &&
337+
paramsOrder.length > 1 &&
338+
a.tag === PARAM &&
339+
b.tag === PARAM
306340
) {
307-
shouldSortAgain = true;
341+
const aIndex = paramsOrder.indexOf(a.name);
342+
const bIndex = paramsOrder.indexOf(b.name);
343+
if (aIndex > -1 && bIndex > -1) {
344+
//sort params
345+
return aIndex - bIndex;
346+
}
347+
return 0;
308348
}
309-
310-
return tagGroup;
349+
return (
350+
getTagOrderWeight(a.tag, options) - getTagOrderWeight(b.tag, options)
351+
);
311352
});
312353

354+
// add an empty line between groups
355+
if (array.length - 1 !== index) {
356+
tagGroup.push(SPACE_TAG_DATA);
357+
}
358+
359+
if (
360+
index > 0 &&
361+
tagGroup[0]?.tag &&
362+
!TAGS_GROUP_HEAD.includes(tagGroup[0].tag)
363+
) {
364+
shouldSortAgain = true;
365+
}
366+
367+
return tagGroup;
368+
});
369+
313370
return shouldSortAgain ? sortTags(tags, paramsOrder, options) : tags;
314371
}
315372

@@ -615,3 +672,44 @@ function assignOptionalAndDefaultToName({
615672
default: default_,
616673
};
617674
}
675+
676+
type ImportDetails = {
677+
/** the spec associated with this import tag */
678+
spec: Spec;
679+
/** the source of the module that types were imported from */
680+
src: string;
681+
defaultImport?: string;
682+
/** the types that were imported */
683+
namedImports: {
684+
name: string;
685+
/** the alias assigned to the type (EX: alias of "B as B0" is "B0") */
686+
alias?: string;
687+
}[];
688+
};
689+
690+
/**
691+
* Extracts the defaultImports, namedImports, and src associated with a given import tag.
692+
*/
693+
function getImportDetails(spec: Spec): ImportDetails | null {
694+
// step 1: capture the default import, named import clause, and src
695+
const match = spec.description.match(
696+
/([^\s\\,\\{\\}]+)?(?:[^\\{\\}]*)\{?([^\\{\\}]*)?\}?(?:\s+from\s+)[\\'\\"](\S)[\\'\\"]/s,
697+
);
698+
if (!match) return null;
699+
700+
const defaultImport = match[1] || "";
701+
const namedImportsClause = match[2] || "";
702+
const src = match[3] || "";
703+
704+
// step 2: get all named imports from the named import section
705+
const typeMatches = namedImportsClause.matchAll(
706+
/([^\s\\,\\{\\}]+)(?:\s+as\s+)?([^\s\\,\\{\\}]+)?/g,
707+
);
708+
709+
const namedImports = [];
710+
for (const typeMatch of typeMatches) {
711+
namedImports.push({ name: typeMatch[1], alias: typeMatch[2] });
712+
}
713+
714+
return { spec, src, namedImports, defaultImport };
715+
}

src/roles.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
FLOW,
2121
FUNCTION,
2222
IGNORE,
23+
IMPORT,
2324
LICENSE,
2425
MEMBER,
2526
MEMBEROF,
@@ -84,6 +85,7 @@ const TAGS_NAMELESS = [
8485
EXAMPLE,
8586
EXTENDS,
8687
LICENSE,
88+
IMPORT,
8789
MODULE,
8890
NAMESPACE,
8991
OVERLOAD,
@@ -106,6 +108,7 @@ const TAGS_TYPELESS = [
106108
DESCRIPTION,
107109
EXAMPLE,
108110
IGNORE,
111+
IMPORT,
109112
LICENSE,
110113
MODULE,
111114
NAMESPACE,
@@ -122,6 +125,7 @@ const TAGS_PEV_FORMATE_DESCRIPTION = [
122125
/** @todo should be formate like jsdoc standard saw https://jsdoc.app/tags-borrows.html */
123126
BORROWS,
124127
...TAGS_DEFAULT,
128+
IMPORT,
125129
MEMBEROF,
126130
MODULE,
127131
SEE,
@@ -132,6 +136,7 @@ const TAGS_DESCRIPTION_NEEDED = [
132136
CATEGORY,
133137
DESCRIPTION,
134138
EXAMPLE,
139+
IMPORT,
135140
PRIVATE_REMARKS,
136141
REMARKS,
137142
SINCE,
@@ -174,6 +179,7 @@ const TAGS_GROUP_CONDITION = [
174179
];
175180

176181
const TAGS_ORDER = {
182+
[IMPORT]: 0,
177183
[REMARKS]: 1,
178184
[PRIVATE_REMARKS]: 2,
179185
[PROVIDES_MODULE]: 3,

src/stringify.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ const stringify = async (
125125
if (useTagTitle) tagString += gap + " ".repeat(descGapAdj);
126126
if (
127127
TAGS_PEV_FORMATE_DESCRIPTION.includes(tag) ||
128-
!TAGS_ORDER[tag as keyof typeof TAGS_ORDER]
128+
TAGS_ORDER[tag as keyof typeof TAGS_ORDER] === undefined
129129
) {
130130
// Avoid wrapping
131131
descriptionString = description;

src/tags.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const FIRES = "fires";
2121
const FLOW = "flow";
2222
const FUNCTION = "function";
2323
const IGNORE = "ignore";
24+
const IMPORT = "import";
2425
const LICENSE = "license";
2526
const MEMBER = "member";
2627
const MEMBEROF = "memberof";
@@ -79,6 +80,7 @@ export {
7980
FLOW,
8081
FUNCTION,
8182
IGNORE,
83+
IMPORT,
8284
LICENSE,
8385
MEMBER,
8486
MEMBEROF,

tests/__snapshots__/typeScript.test.ts.snap

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,21 @@ exports[`max width challenge 1`] = `
158158
}
159159
"
160160
`;
161+
162+
exports[`type imports 1`] = `
163+
"/**
164+
* @import {A} from "a"
165+
* @import BMain, {
166+
* B as B1,
167+
* B2,
168+
* B3,
169+
* B4
170+
* } from "b"
171+
* @typedef {Object} Foo
172+
*/
173+
/**
174+
* @import BDefault, {B5} from "b"
175+
* @import C from "c"
176+
*/
177+
"
178+
`;

tests/typeScript.test.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ test("hoisted object", async () => {
8282
}
8383
} User
8484
*/
85-
85+
8686
`);
8787

8888
expect(result).toMatchSnapshot();
@@ -122,10 +122,10 @@ class test {
122122
* @returns {StarkStringType & NativeString}
123123
*/
124124
testFunction(){
125-
125+
126126
}
127127
}
128-
128+
129129
this._value = this._value.replace(searchValue, replaceValue);
130130
return this;
131131
}
@@ -190,3 +190,23 @@ test("Long type Union types", async () => {
190190

191191
expect(result).toMatchSnapshot();
192192
});
193+
194+
test.only("type imports", async () => {
195+
const result = await subject(
196+
`
197+
/**
198+
* @typedef {Object} Foo
199+
* @import {A} from 'a'
200+
* @import BM, { B as B1,
201+
* B2 , B4 } from 'b'
202+
* @import BMain, {B3 } from 'b'
203+
*/
204+
/**
205+
* @import BDefault, { B5 } from 'b'
206+
* @import C from 'c'
207+
*/
208+
`,
209+
);
210+
211+
expect(result).toMatchSnapshot();
212+
});

0 commit comments

Comments
 (0)