Skip to content

Commit 82ad8db

Browse files
committed
Add support for @sortStrategy
Resolves #2965
1 parent e48c6be commit 82ad8db

File tree

13 files changed

+193
-21
lines changed

13 files changed

+193
-21
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ title: Changelog
44

55
## Unreleased
66

7+
### Features
8+
9+
- Introduced the `@sortStrategy` tag to override the `sort` option on a specific reflection, #2965.
10+
711
### Bug Fixes
812

913
- Classes and functions exported with `export { type X }` are no longer missing comments, #2970.

site/options/organization.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ Specifies the sort order for members. Sorting strategies will be applied in orde
8181
If an earlier sorting strategy determines the relative ordering of two reflections, later
8282
ordering strategies will not be applied.
8383

84+
This option defines the default sort strategy, the [`@sortStrategy`](../tags/sortStrategy.md)
85+
tag may be used to override it for individual reflections.
86+
8487
For example, with the setting `["static-first", "visibility"]`, TypeDoc will first compare two
8588
reflections by if they are static or not, and if that comparison returns equal, will check the
8689
visibility of each reflection. On the other hand, if `["visibility", "static-first"]` is specified,
@@ -104,6 +107,18 @@ The available sorting strategies are:
104107
- `documents-last`
105108
- `alphabetical-ignoring-documents`
106109

110+
The default sort order is:
111+
112+
```json
113+
{
114+
"sort": [
115+
"kind",
116+
"instance-first",
117+
"alphabetical-ignoring-documents"
118+
]
119+
}
120+
```
121+
107122
## sortEntryPoints
108123

109124
```bash

site/tags.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ children:
4747
- tags/remarks.md
4848
- tags/returns.md
4949
- tags/sealed.md
50-
- tags/since.md
5150
- tags/see.md
51+
- tags/since.md
52+
- tags/sortStrategy.md
5253
- tags/summary.md
5354
- tags/template.md
5455
- tags/throws.md
@@ -130,6 +131,7 @@ examples for how to use the export ([`@example`](./tags/example.md)).
130131
- [`@returns`, `@return`](./tags/returns.md)
131132
- [`@see`](./tags/see.md)
132133
- [`@since`](./tags/since.md)
134+
- [`@sortStrategy`](./tags/sortStrategy.md)
133135
- [`@summary`](./tags/summary.md)
134136
- [`@template`](./tags/template.md)
135137
- [`@throws`](./tags/throws.md)

site/tags/sortStrategy.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
title: "@sortStrategy"
3+
---
4+
5+
# @sortStrategy
6+
7+
**Tag Kind:** [Block](../tags.md#Block-tags) <br>
8+
9+
This tag can be used to override the [sort](../options/organization.md#sort) locally
10+
for a module, namespace, class, or interface. The override will be applied to direct
11+
children of the declaration it appears on. If the declaration has a child which contains
12+
children (e.g. a nested namespace) the grandchildren will _not_ be sorted according
13+
to the `@sortStrategy` tag.
14+
15+
## Example
16+
17+
This class makes the most sense if the documentation is reviewed in the source order
18+
rather than being sorted alphabetically.
19+
20+
```ts
21+
/**
22+
* @sortStrategy source-order
23+
*/
24+
export class Class {
25+
commonMethod(): void;
26+
commonMethod2(): void;
27+
lessCommonMethod(): void;
28+
uncommonMethod(): void;
29+
}
30+
```
31+
32+
## See Also
33+
34+
- The [`--sort`](../options/organization.md#sort) option

src/lib/converter/plugins/CategoryPlugin.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@ import type { Context } from "../context.js";
1515
import { ConverterEvents } from "../converter-events.js";
1616
import type { Converter } from "../converter.js";
1717
import { i18n } from "#utils";
18+
import { isValidSortStrategy } from "../../utils/sort.js";
1819

1920
/**
2021
* A handler that sorts and categorizes the found reflections in the resolving phase.
2122
*
2223
* The handler sets the ´category´ property of all reflections.
2324
*/
2425
export class CategoryPlugin extends ConverterComponent {
25-
sortFunction!: (
26+
defaultSortFunction!: (
2627
reflections: Array<DeclarationReflection | DocumentReflection>,
2728
) => void;
2829

@@ -71,7 +72,7 @@ export class CategoryPlugin extends ConverterComponent {
7172
* Triggered when the converter begins converting a project.
7273
*/
7374
private setup() {
74-
this.sortFunction = getSortFunction(this.application.options);
75+
this.defaultSortFunction = getSortFunction(this.application.options);
7576

7677
// Set up static properties
7778
if (this.defaultCategory) {
@@ -203,12 +204,25 @@ export class CategoryPlugin extends ConverterComponent {
203204
}
204205

205206
for (const cat of categories.values()) {
206-
this.sortFunction(cat.children);
207+
this.getSortFunction(parent)(cat.children);
207208
}
208209

209210
return Array.from(categories.values());
210211
}
211212

213+
getSortFunction(reflection: ContainerReflection) {
214+
const tag = reflection.comment?.getTag("@sortStrategy");
215+
if (tag) {
216+
const text = Comment.combineDisplayParts(tag.content);
217+
// We don't need to warn about invalid strategies here because the group plugin
218+
// runs first and will have already warned.
219+
const strategies = text.split(/[,\s]+/).filter(isValidSortStrategy);
220+
return getSortFunction(this.application.options, strategies);
221+
}
222+
223+
return this.defaultSortFunction;
224+
}
225+
212226
/**
213227
* Callback used to sort categories by name.
214228
*

src/lib/converter/plugins/GroupPlugin.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ import {
99
import { ReflectionGroup } from "../../models/ReflectionGroup.js";
1010
import { ConverterComponent } from "../components.js";
1111
import type { Context } from "../context.js";
12-
import { getSortFunction } from "../../utils/sort.js";
13-
import { Option } from "../../utils/index.js";
12+
import { getSortFunction, isValidSortStrategy, SORT_STRATEGIES } from "../../utils/sort.js";
13+
import { Option, type SortStrategy } from "../../utils/index.js";
1414
import { Comment } from "../../models/index.js";
1515
import { ConverterEvents } from "../converter-events.js";
1616
import type { Converter } from "../converter.js";
1717
import { ApplicationEvents } from "../../application-events.js";
1818
import assert from "assert";
19-
import { i18n } from "#utils";
19+
import { i18n, partition } from "#utils";
2020

2121
// Same as the defaultKindSortOrder in sort.ts
2222
const defaultGroupOrder = [
@@ -47,7 +47,7 @@ const defaultGroupOrder = [
4747
* The handler sets the `groups` property of all container reflections.
4848
*/
4949
export class GroupPlugin extends ConverterComponent {
50-
sortFunction!: (
50+
defaultSortFunction!: (
5151
reflections: Array<DeclarationReflection | DocumentReflection>,
5252
) => void;
5353

@@ -107,27 +107,29 @@ export class GroupPlugin extends ConverterComponent {
107107
}
108108

109109
private setup() {
110-
this.sortFunction = getSortFunction(this.application.options);
110+
this.defaultSortFunction = getSortFunction(this.application.options);
111111
GroupPlugin.WEIGHTS = this.groupOrder;
112112
if (GroupPlugin.WEIGHTS.length === 0) {
113113
GroupPlugin.WEIGHTS = defaultGroupOrder.map((kind) => ReflectionKind.pluralString(kind));
114114
}
115115
}
116116

117117
private group(reflection: ContainerReflection) {
118+
const sortFunction = this.getSortFunction(reflection);
119+
118120
if (reflection.childrenIncludingDocuments && !reflection.groups) {
119121
if (reflection.children) {
120122
if (
121123
this.sortEntryPoints ||
122124
!reflection.children.some((c) => c.kindOf(ReflectionKind.Module))
123125
) {
124-
this.sortFunction(reflection.children);
125-
this.sortFunction(reflection.documents || []);
126-
this.sortFunction(reflection.childrenIncludingDocuments);
126+
sortFunction(reflection.children);
127+
sortFunction(reflection.documents || []);
128+
sortFunction(reflection.childrenIncludingDocuments);
127129
}
128130
} else if (reflection.documents) {
129-
this.sortFunction(reflection.documents);
130-
this.sortFunction(reflection.childrenIncludingDocuments);
131+
sortFunction(reflection.documents);
132+
sortFunction(reflection.childrenIncludingDocuments);
131133
}
132134

133135
if (reflection.comment?.hasModifier("@disableGroups")) {
@@ -256,6 +258,25 @@ export class GroupPlugin extends ConverterComponent {
256258
return Array.from(groups.values()).sort(GroupPlugin.sortGroupCallback);
257259
}
258260

261+
getSortFunction(reflection: ContainerReflection) {
262+
const tag = reflection.comment?.getTag("@sortStrategy");
263+
if (tag) {
264+
const text = Comment.combineDisplayParts(tag.content);
265+
const strategies = text.split(/[,\s]+/);
266+
const [valid, invalid] = partition(strategies, isValidSortStrategy);
267+
for (const inv of invalid) {
268+
this.application.logger.warn(i18n.comment_for_0_specifies_1_as_sort_strategy_but_only_2_is_valid(
269+
reflection.getFriendlyFullName(),
270+
inv,
271+
SORT_STRATEGIES.join("\n\t"),
272+
));
273+
}
274+
return getSortFunction(this.application.options, valid as SortStrategy[]);
275+
}
276+
277+
return this.defaultSortFunction;
278+
}
279+
259280
/**
260281
* Callback used to sort groups by name.
261282
*/

src/lib/internationalization/locales/en.cts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ export = {
123123
`Comment for {0} includes @categoryDescription for "{1}", but no child is placed in that category`,
124124
comment_for_0_includes_groupDescription_for_1_but_no_child_in_group:
125125
`Comment for {0} includes @groupDescription for "{1}", but no child is placed in that group`,
126+
comment_for_0_specifies_1_as_sort_strategy_but_only_2_is_valid:
127+
`Comment for {0} specifies @sortStrategy with "{1}", which is an invalid sort strategy, the following are valid:\n\t{2}`,
126128
label_0_for_1_cannot_be_referenced:
127129
`The label "{0}" for {1} cannot be referenced with a declaration reference. Labels may only contain A-Z, 0-9, and _, and may not start with a number`,
128130
modifier_tag_0_is_mutually_exclusive_with_1_in_comment_for_2:
@@ -419,7 +421,7 @@ export = {
419421
option_0_values_must_be_numbers: "All values of {0} must be numbers",
420422
option_0_values_must_be_array_of_tags: "{0} must be an array of valid tag names",
421423
option_0_specified_1_but_only_2_is_valid:
422-
`{0} may only specify known values, and invalid values were provided ({1}). The valid sort strategies are:\n{2}`,
424+
`{0} may only specify known values, and invalid values were provided ({1}). The valid options are:\n{2}`,
423425
option_outputs_must_be_array:
424426
`"outputs" option must be an array of { name: string, path: string, options?: TypeDocOptions } values.`,
425427
specified_output_0_has_not_been_defined: `Specified output "{0}" has not been defined.`,

src/lib/utils/options/sources/typedoc.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -845,10 +845,7 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
845845
type: ParameterType.Array,
846846
defaultValue: OptionDefaults.sort,
847847
validate(value) {
848-
const invalid = new Set(value);
849-
for (const v of SORT_STRATEGIES) {
850-
invalid.delete(v);
851-
}
848+
const invalid = setDifference(value, SORT_STRATEGIES);
852849

853850
if (invalid.size !== 0) {
854851
throw new Error(

src/lib/utils/options/tsdoc-defaults.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const blockTags = [
3737
"@return",
3838
"@satisfies",
3939
"@since",
40+
"@sortStrategy",
4041
"@template", // Alias for @typeParam
4142
"@type",
4243
"@typedef",

src/lib/utils/sort.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,11 @@ const sorts: Record<
163163
},
164164
};
165165

166-
export function getSortFunction(opts: Options) {
166+
export function isValidSortStrategy(strategy: string): strategy is SortStrategy {
167+
return SORT_STRATEGIES.includes(strategy as never);
168+
}
169+
170+
export function getSortFunction(opts: Options, strategies: readonly SortStrategy[] = opts.getValue("sort")) {
167171
const kindSortOrder = opts
168172
.getValue("kindSortOrder")
169173
.map((k) => ReflectionKind[k]);
@@ -174,7 +178,6 @@ export function getSortFunction(opts: Options) {
174178
}
175179
}
176180

177-
const strategies = opts.getValue("sort");
178181
const data = { kindSortOrder };
179182

180183
return function sortReflections(

src/test/behavior.c2.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,4 +1486,46 @@ describe("Behavior Tests", () => {
14861486
{ kind: "text", text: ")" },
14871487
]);
14881488
});
1489+
1490+
it("Supports the @sortStrategy tag #2965", () => {
1491+
const project = convert("sortStrategyTag");
1492+
1493+
const getOrder = (name: string) => query(project, name).children?.map(c => c.name);
1494+
1495+
equal(getOrder("A"), ["b", "c", "a"]);
1496+
equal(getOrder("B"), ["a", "b", "c"]);
1497+
equal(getOrder("C"), ["b", "c", "a"]);
1498+
1499+
const getGroupOrders = (name: string) =>
1500+
query(project, name).groups?.map(g => [g.title, g.children.map(c => c.name)]);
1501+
1502+
equal(getGroupOrders("A"), [
1503+
["Variables", ["b", "a"]],
1504+
["Functions", ["c"]],
1505+
]);
1506+
1507+
equal(getGroupOrders("B"), [
1508+
["Variables", ["a", "b"]],
1509+
["Functions", ["c"]],
1510+
]);
1511+
1512+
equal(getGroupOrders("C"), [
1513+
["Variables", ["b", "c"]],
1514+
["Functions", ["a"]],
1515+
]);
1516+
1517+
const getCategoryOrders = (name: string) =>
1518+
query(project, name).categories?.map(g => [g.title, g.children.map(c => c.name)]);
1519+
1520+
equal(getCategoryOrders("D"), [
1521+
["Cat", ["b", "a", "c"]],
1522+
]);
1523+
1524+
logger.expectMessage(
1525+
`warn: Comment for E specifies @sortStrategy with "invalid", which is an invalid sort strategy*`,
1526+
);
1527+
logger.expectMessage(
1528+
`warn: Comment for E specifies @sortStrategy with "invalid2", which is an invalid sort strategy*`,
1529+
);
1530+
});
14891531
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/** @sortStrategy source-order */
2+
export namespace A {
3+
export const b = 1;
4+
export function c() {}
5+
export const a = 2;
6+
}
7+
8+
/** @sortStrategy alphabetical */
9+
export namespace B {
10+
export function c() {}
11+
export const b = 1;
12+
export const a = 1;
13+
}
14+
15+
// Default is kind then alphabetical
16+
export namespace C {
17+
export const c = 1;
18+
export function a() {}
19+
export const b = 1;
20+
}
21+
22+
/** @sortStrategy source-order */
23+
export namespace D {
24+
/** @category Cat */
25+
export const b = 1;
26+
/** @category Cat */
27+
export const a = 1;
28+
/** @category Cat */
29+
export const c = 1;
30+
}
31+
32+
/** @sortStrategy invalid, source-order, invalid2 */
33+
export namespace E {}

tsdoc.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,10 @@
237237
{
238238
"tagName": "@useDeclaredType",
239239
"syntaxKind": "modifier"
240+
},
241+
{
242+
"tagName": "@sortStrategy",
243+
"syntaxKind": "block"
240244
}
241245
]
242246
}

0 commit comments

Comments
 (0)