From e1254843bcd34202d7277ef0e311b56cf9db869d Mon Sep 17 00:00:00 2001 From: Gerrit Birkeland Date: Mon, 31 May 2021 20:25:56 -0600 Subject: [PATCH] feat: Add support for sorting reflections based on user criteria Resolves #112 --- src/index.ts | 5 +- src/lib/application.ts | 9 +- src/lib/converter/plugins/GroupPlugin.ts | 64 +---- src/lib/models/reflections/abstract.ts | 10 + src/lib/utils/index.ts | 29 ++- src/lib/utils/options/declaration.ts | 2 + src/lib/utils/options/sources/typedoc.ts | 25 ++ src/lib/utils/sort.ts | 154 ++++++++++++ src/test/converter.test.ts | 1 + src/test/converter2.test.ts | 1 + src/test/renderer.test.ts | 1 + src/test/utils/options/declaration.test.ts | 61 +---- .../utils/options/default-options.test.ts | 34 +++ src/test/utils/sort.test.ts | 233 ++++++++++++++++++ 14 files changed, 505 insertions(+), 124 deletions(-) create mode 100644 src/lib/utils/sort.ts create mode 100644 src/test/utils/options/default-options.test.ts create mode 100644 src/test/utils/sort.test.ts diff --git a/src/index.ts b/src/index.ts index bfc06e014..b320b25ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,7 @@ export { TSConfigReader, TypeDocReader, ArgumentsReader, -} from "./lib/utils/options"; +} from "./lib/utils"; export type { OptionsReader, @@ -37,7 +37,8 @@ export type { MixedDeclarationOption, MapDeclarationOption, DeclarationOptionToOptionType, -} from "./lib/utils/options"; + SortStrategy, +} from "./lib/utils"; export { JSONOutput } from "./lib/serialization"; diff --git a/src/lib/application.ts b/src/lib/application.ts index 53bbb05f7..bc1329dcb 100644 --- a/src/lib/application.ts +++ b/src/lib/application.ts @@ -141,11 +141,10 @@ export class Application extends ChildableComponent< } this.logger.level = this.options.getValue("logLevel"); - let plugins = this.options.getValue("plugin"); - if (plugins.length === 0) { - plugins = discoverNpmPlugins(this); - } - loadPlugins(this, this.options.getValue("plugin")); + const plugins = this.options.isSet("plugin") + ? this.options.getValue("plugin") + : discoverNpmPlugins(this); + loadPlugins(this, plugins); this.options.reset(); for (const [key, val] of Object.entries(options)) { diff --git a/src/lib/converter/plugins/GroupPlugin.ts b/src/lib/converter/plugins/GroupPlugin.ts index 4b3894f66..b7dca10a9 100644 --- a/src/lib/converter/plugins/GroupPlugin.ts +++ b/src/lib/converter/plugins/GroupPlugin.ts @@ -9,6 +9,7 @@ import { SourceDirectory } from "../../models/sources/directory"; import { Component, ConverterComponent } from "../components"; import { Converter } from "../converter"; import { Context } from "../context"; +import { sortReflections } from "../../utils/sort"; /** * A handler that sorts and groups the found reflections in the resolving phase. @@ -17,38 +18,6 @@ import { Context } from "../context"; */ @Component({ name: "group" }) export class GroupPlugin extends ConverterComponent { - /** - * Define the sort order of reflections. - */ - static WEIGHTS = [ - ReflectionKind.Project, - ReflectionKind.Module, - ReflectionKind.Namespace, - ReflectionKind.Enum, - ReflectionKind.EnumMember, - ReflectionKind.Class, - ReflectionKind.Interface, - ReflectionKind.TypeAlias, - - ReflectionKind.Constructor, - ReflectionKind.Event, - ReflectionKind.Property, - ReflectionKind.Variable, - ReflectionKind.Function, - ReflectionKind.Accessor, - ReflectionKind.Method, - ReflectionKind.ObjectLiteral, - - ReflectionKind.Parameter, - ReflectionKind.TypeParameter, - ReflectionKind.TypeLiteral, - ReflectionKind.CallSignature, - ReflectionKind.ConstructorSignature, - ReflectionKind.IndexSignature, - ReflectionKind.GetSignature, - ReflectionKind.SetSignature, - ]; - /** * Define the singular name of individual reflection kinds. */ @@ -123,7 +92,10 @@ export class GroupPlugin extends ConverterComponent { reflection.children.length > 0 && !reflection.groups ) { - reflection.children.sort(GroupPlugin.sortCallback); + sortReflections( + reflection.children, + this.application.options.getValue("sort") + ); reflection.groups = GroupPlugin.getReflectionGroups( reflection.children ); @@ -234,30 +206,4 @@ export class GroupPlugin extends ConverterComponent { return this.getKindString(kind) + "s"; } } - - /** - * Callback used to sort reflections by weight defined by ´GroupPlugin.WEIGHTS´ and name. - * - * @param a The left reflection to sort. - * @param b The right reflection to sort. - * @returns The sorting weight. - */ - static sortCallback(a: Reflection, b: Reflection): number { - const aWeight = GroupPlugin.WEIGHTS.indexOf(a.kind); - const bWeight = GroupPlugin.WEIGHTS.indexOf(b.kind); - if (aWeight === bWeight) { - if (a.flags.isStatic && !b.flags.isStatic) { - return 1; - } - if (!a.flags.isStatic && b.flags.isStatic) { - return -1; - } - if (a.name === b.name) { - return 0; - } - return a.name > b.name ? 1 : -1; - } else { - return aWeight - bWeight; - } - } } diff --git a/src/lib/models/reflections/abstract.ts b/src/lib/models/reflections/abstract.ts index 3ddfff48a..95f82c78b 100644 --- a/src/lib/models/reflections/abstract.ts +++ b/src/lib/models/reflections/abstract.ts @@ -1,3 +1,4 @@ +import { ok } from "assert"; import { SourceReference } from "../sources/file"; import { Type } from "../types/index"; import { Comment } from "../comments/comment"; @@ -366,6 +367,15 @@ export abstract class Reflection { */ parent?: Reflection; + get project(): ProjectReflection { + if (this.isProject()) return this; + ok( + this.parent, + "Tried to get the project on a reflection not in a project" + ); + return this.parent.project; + } + /** * The parsed documentation comment attached to this reflection. */ diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 5f8fe8499..903dd0c7e 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -1,6 +1,30 @@ export type { IfInternal, NeverIfInternal } from "./general"; -export { Options, ParameterType, ParameterHint, BindOption } from "./options"; +export { + Options, + ParameterType, + ParameterHint, + BindOption, + TSConfigReader, + TypeDocReader, + ArgumentsReader, +} from "./options"; +export type { + OptionsReader, + TypeDocOptions, + TypeDocOptionMap, + KeyToDeclaration, + DeclarationOption, + DeclarationOptionBase, + StringDeclarationOption, + NumberDeclarationOption, + BooleanDeclarationOption, + ArrayDeclarationOption, + MixedDeclarationOption, + MapDeclarationOption, + DeclarationOptionToOptionType, +} from "./options"; + export { insertPrioritySorted, removeIfPresent, @@ -23,3 +47,6 @@ export { } from "./fs"; export { Logger, LogLevel, ConsoleLogger, CallbackLogger } from "./loggers"; export { loadPlugins, discoverNpmPlugins } from "./plugins"; + +export { sortReflections } from "./sort"; +export type { SortStrategy } from "./sort"; diff --git a/src/lib/utils/options/declaration.ts b/src/lib/utils/options/declaration.ts index baa7a87b5..e55d12e8f 100644 --- a/src/lib/utils/options/declaration.ts +++ b/src/lib/utils/options/declaration.ts @@ -1,5 +1,6 @@ import { Theme as ShikiTheme } from "shiki"; import { LogLevel } from "../loggers"; +import { SortStrategy } from "../sort"; /** * An interface describing all TypeDoc specific options. Generated from a @@ -66,6 +67,7 @@ export interface TypeDocOptionMap { defaultCategory: string; categoryOrder: string[]; categorizeByGroup: boolean; + sort: SortStrategy[]; gitRevision: string; gitRemote: string; gaID: string; diff --git a/src/lib/utils/options/sources/typedoc.ts b/src/lib/utils/options/sources/typedoc.ts index 9802a4427..40c12007c 100644 --- a/src/lib/utils/options/sources/typedoc.ts +++ b/src/lib/utils/options/sources/typedoc.ts @@ -2,6 +2,7 @@ import { Options } from ".."; import { LogLevel } from "../../loggers"; import { ParameterType, ParameterHint } from "../declaration"; import { BUNDLED_THEMES } from "shiki"; +import { SORT_STRATEGIES } from "../../sort"; export function addTypeDocOptions(options: Pick) { options.addDeclaration({ @@ -188,6 +189,30 @@ export function addTypeDocOptions(options: Pick) { type: ParameterType.Boolean, defaultValue: true, }); + options.addDeclaration({ + name: "sort", + help: "Specify the sort strategy for documented values", + type: ParameterType.Array, + defaultValue: ["kind", "instance-first", "alphabetical"], + validate(value) { + const invalid = new Set(value); + for (const v of SORT_STRATEGIES) { + invalid.delete(v); + } + + if (invalid.size !== 0) { + throw new Error( + `sort may only specify known values, and invalid values were provided (${Array.from( + invalid + ).join( + ", " + )}). The valid sort strategies are:\n${SORT_STRATEGIES.join( + ", " + )}` + ); + } + }, + }); options.addDeclaration({ name: "gitRevision", help: diff --git a/src/lib/utils/sort.ts b/src/lib/utils/sort.ts new file mode 100644 index 000000000..6cb360a33 --- /dev/null +++ b/src/lib/utils/sort.ts @@ -0,0 +1,154 @@ +/** + * Module which handles sorting reflections according to a user specified strategy. + * @module + */ + +import { DeclarationReflection, ReflectionKind } from "../models"; + +export const SORT_STRATEGIES = [ + "source-order", + "alphabetical", + "enum-value-ascending", + "enum-value-descending", + "static-first", + "instance-first", + "visibility", + "required-first", + "kind", +] as const; + +export type SortStrategy = typeof SORT_STRATEGIES[number]; + +// Return true if a < b +const sorts: Record< + SortStrategy, + (a: DeclarationReflection, b: DeclarationReflection) => boolean +> = { + "source-order"(a, b) { + const aSymbol = a.project.getSymbolFromReflection(a); + const bSymbol = b.project.getSymbolFromReflection(b); + + // This is going to be somewhat ambiguous. No way around that. Treat the first + // declaration of a symbol as its ordering declaration. + const aDecl = aSymbol?.getDeclarations()?.[0]; + const bDecl = bSymbol?.getDeclarations()?.[0]; + + if (aDecl && bDecl) { + const aFile = aDecl.getSourceFile().fileName; + const bFile = bDecl.getSourceFile().fileName; + if (aFile < bFile) { + return true; + } + if (aFile == bFile && aDecl.pos < bDecl.pos) { + return true; + } + + return false; + } + + // Someone is doing something weird. Fail to re-order. This *might* be a bug in TD + // but it could also be TS having some exported symbol without a declaration. + return false; + }, + alphabetical(a, b) { + return a.name < b.name; + }, + "enum-value-ascending"(a, b) { + if ( + a.kind == ReflectionKind.EnumMember && + b.kind == ReflectionKind.EnumMember + ) { + return ( + parseFloat(a.defaultValue ?? "0") < + parseFloat(b.defaultValue ?? "0") + ); + } + return false; + }, + "enum-value-descending"(a, b) { + if ( + a.kind == ReflectionKind.EnumMember && + b.kind == ReflectionKind.EnumMember + ) { + return ( + parseFloat(b.defaultValue ?? "0") < + parseFloat(a.defaultValue ?? "0") + ); + } + return false; + }, + "static-first"(a, b) { + return a.flags.isStatic && !b.flags.isStatic; + }, + "instance-first"(a, b) { + return !a.flags.isStatic && b.flags.isStatic; + }, + visibility(a, b) { + // Note: flags.isPublic may not be set on public members. It will only be set + // if the user explicitly marks members as public. Therefore, we can't use it + // here to get a reliable sort order. + if (a.flags.isPrivate) { + return false; // Not sorted before anything + } + if (a.flags.isProtected) { + return b.flags.isPrivate; // Sorted before privates + } + if (b.flags.isPrivate || b.flags.isProtected) { + return true; // We are public, sort before b if b is less visible + } + return false; + }, + "required-first"(a, b) { + return !a.flags.isOptional && b.flags.isOptional; + }, + kind(a, b) { + const weights = [ + ReflectionKind.Reference, + ReflectionKind.Project, + ReflectionKind.Module, + ReflectionKind.Namespace, + ReflectionKind.Enum, + ReflectionKind.EnumMember, + ReflectionKind.Class, + ReflectionKind.Interface, + ReflectionKind.TypeAlias, + + ReflectionKind.Constructor, + ReflectionKind.Event, + ReflectionKind.Property, + ReflectionKind.Variable, + ReflectionKind.Function, + ReflectionKind.Accessor, + ReflectionKind.Method, + ReflectionKind.ObjectLiteral, + + ReflectionKind.Parameter, + ReflectionKind.TypeParameter, + ReflectionKind.TypeLiteral, + ReflectionKind.CallSignature, + ReflectionKind.ConstructorSignature, + ReflectionKind.IndexSignature, + ReflectionKind.GetSignature, + ReflectionKind.SetSignature, + ] as const; + + return weights.indexOf(a.kind) < weights.indexOf(b.kind); + }, +}; + +export function sortReflections( + strategies: DeclarationReflection[], + strats: readonly SortStrategy[] +) { + strategies.sort((a, b) => { + for (const s of strats) { + if (sorts[s](a, b)) { + return -1; + } + if (sorts[s](b, a)) { + return 1; + } + } + return 0; + }); +} diff --git a/src/test/converter.test.ts b/src/test/converter.test.ts index 902179006..aef5a84b3 100644 --- a/src/test/converter.test.ts +++ b/src/test/converter.test.ts @@ -21,6 +21,7 @@ describe("Converter", function () { disableSources: true, tsconfig: Path.join(base, "tsconfig.json"), externalPattern: ["**/node_modules/**"], + plugin: [], }); let program: ts.Program; diff --git a/src/test/converter2.test.ts b/src/test/converter2.test.ts index 786463574..5ca3d69c8 100644 --- a/src/test/converter2.test.ts +++ b/src/test/converter2.test.ts @@ -216,6 +216,7 @@ describe("Converter2", () => { excludeExternals: true, disableSources: true, tsconfig: join(base, "tsconfig.json"), + plugin: [], }); let program: ts.Program; diff --git a/src/test/renderer.test.ts b/src/test/renderer.test.ts index d2cd8a00e..83b4fba38 100644 --- a/src/test/renderer.test.ts +++ b/src/test/renderer.test.ts @@ -73,6 +73,7 @@ describe("Renderer", function () { name: "typedoc", disableSources: true, tsconfig: Path.join(src, "..", "tsconfig.json"), + plugin: [], }); app.options.setValue("entryPoints", app.expandInputFiles([src])); }); diff --git a/src/test/utils/options/declaration.test.ts b/src/test/utils/options/declaration.test.ts index fa3ac5427..d150e6da4 100644 --- a/src/test/utils/options/declaration.test.ts +++ b/src/test/utils/options/declaration.test.ts @@ -79,7 +79,7 @@ describe("Options - Default convert function", () => { ); }); - it("Generates no error for a number option if the validation function doesn't throw one", () => { + it("Validates number options", () => { const declaration: NumberDeclarationOption = { name: "test", help: "", @@ -93,19 +93,6 @@ describe("Options - Default convert function", () => { equal(convert(0, declaration), 0); equal(convert(2, declaration), 2); equal(convert(4, declaration), 4); - }); - - it("Generates an error for a number option if the validation function throws one", () => { - const declaration: NumberDeclarationOption = { - name: "test", - help: "", - type: ParameterType.Number, - validate: (value: number) => { - if (value % 2 !== 0) { - throw new Error("test must be even"); - } - }, - }; throws(() => convert(1, declaration), new Error("test must be even")); }); @@ -117,7 +104,7 @@ describe("Options - Default convert function", () => { equal(convert(void 0, optionWithType(ParameterType.String)), ""); }); - it("Generates no error for a string option if the validation function doesn't throw one", () => { + it("Validates string options", () => { const declaration: StringDeclarationOption = { name: "test", help: "", @@ -129,19 +116,6 @@ describe("Options - Default convert function", () => { }, }; equal(convert("TOASTY", declaration), "TOASTY"); - }); - - it("Generates an error for a string option if the validation function throws one", () => { - const declaration: StringDeclarationOption = { - name: "test", - help: "", - type: ParameterType.String, - validate: (value: string) => { - if (value !== value.toUpperCase()) { - throw new Error("test must be upper case"); - } - }, - }; throws( () => convert("toasty", declaration), new Error("test must be upper case") @@ -163,7 +137,7 @@ describe("Options - Default convert function", () => { equal(convert(true, optionWithType(ParameterType.Array)), []); }); - it("Generates no error for an array option if the validation function doesn't throw one", () => { + it("Validates array options", () => { const declaration: ArrayDeclarationOption = { name: "test", help: "", @@ -176,19 +150,6 @@ describe("Options - Default convert function", () => { }; equal(convert(["1"], declaration), ["1"]); equal(convert(["1", "2"], declaration), ["1", "2"]); - }); - - it("Generates an error for an array option if the validation function throws one", () => { - const declaration: ArrayDeclarationOption = { - name: "test", - help: "", - type: ParameterType.Array, - validate: (value: string[]) => { - if (value.length === 0) { - throw new Error("test must not be empty"); - } - }, - }; throws( () => convert([], declaration), new Error("test must not be empty") @@ -282,7 +243,7 @@ describe("Options - Default convert function", () => { equal(convert(data, optionWithType(ParameterType.Mixed)), data); }); - it("Generates no error for a mixed option if the validation function doesn't throw one", () => { + it("Validates mixed options", () => { const declaration: MixedDeclarationOption = { name: "test", help: "", @@ -295,20 +256,6 @@ describe("Options - Default convert function", () => { }, }; equal(convert("text", declaration), "text"); - }); - - it("Generates an error for a mixed option if the validation function throws one", () => { - const declaration: MixedDeclarationOption = { - name: "test", - help: "", - type: ParameterType.Mixed, - defaultValue: "default", - validate: (value: unknown) => { - if (typeof value === "number") { - throw new Error("test must not be a number"); - } - }, - }; throws( () => convert(1, declaration), new Error("test must not be a number") diff --git a/src/test/utils/options/default-options.test.ts b/src/test/utils/options/default-options.test.ts new file mode 100644 index 000000000..60f32c50f --- /dev/null +++ b/src/test/utils/options/default-options.test.ts @@ -0,0 +1,34 @@ +import { ok, throws, strictEqual } from "assert"; +import { BUNDLED_THEMES, Theme } from "shiki"; +import { Logger, Options } from "../../../lib/utils"; + +describe("Default Options", () => { + const opts = new Options(new Logger()); + opts.addDefaultDeclarations(); + + describe("highlightTheme", () => { + it("Errors if an invalid theme is provided", () => { + // @ts-expect-error setValue should require a valid theme. + throws(() => opts.setValue("highlightTheme", "randomTheme")); + opts.setValue("highlightTheme", BUNDLED_THEMES[0] as Theme); + strictEqual(opts.getValue("highlightTheme"), BUNDLED_THEMES[0]); + }); + }); + + describe("sort", () => { + it("Errors if an invalid sort version is provided", () => { + // @ts-expect-error setValue should require a valid sort version. + throws(() => opts.setValue("sort", ["random", "alphabetical"])); + }); + + it("Reports which sort option(s) was invalid", () => { + try { + // @ts-expect-error setValue should require a valid sort version. + opts.setValue("sort", ["random", "alphabetical", "foo"]); + } catch (e) { + ok(e.message.includes("random")); + ok(e.message.includes("foo")); + } + }); + }); +}); diff --git a/src/test/utils/sort.test.ts b/src/test/utils/sort.test.ts new file mode 100644 index 000000000..562144b2a --- /dev/null +++ b/src/test/utils/sort.test.ts @@ -0,0 +1,233 @@ +import { deepStrictEqual as equal } from "assert"; +import { + DeclarationReflection, + ProjectReflection, + ReflectionFlag, + ReflectionKind, +} from "../../lib/models"; +import { resetReflectionID } from "../../lib/models/reflections/abstract"; +import { sortReflections } from "../../lib/utils"; + +describe("Sort", () => { + it("Should sort by name", () => { + const arr = [ + new DeclarationReflection("a", ReflectionKind.TypeAlias), + new DeclarationReflection("c", ReflectionKind.TypeAlias), + new DeclarationReflection("b", ReflectionKind.TypeAlias), + ]; + + sortReflections(arr, ["alphabetical"]); + equal( + arr.map((r) => r.name), + ["a", "b", "c"] + ); + }); + + it("Should sort by enum value ascending", () => { + const arr = [ + new DeclarationReflection("a", ReflectionKind.EnumMember), + new DeclarationReflection("b", ReflectionKind.EnumMember), + new DeclarationReflection("c", ReflectionKind.EnumMember), + ]; + arr[0].defaultValue = "123"; + arr[1].defaultValue = "12"; + arr[2].defaultValue = "3"; + + sortReflections(arr, ["enum-value-ascending"]); + equal( + arr.map((r) => r.name), + ["c", "b", "a"] + ); + }); + + it("Should not sort enum value ascending if not an enum member", () => { + const arr = [ + new DeclarationReflection("a", ReflectionKind.Function), + new DeclarationReflection("b", ReflectionKind.EnumMember), + new DeclarationReflection("c", ReflectionKind.EnumMember), + ]; + arr[0].defaultValue = "123"; + arr[1].defaultValue = "12"; + arr[2].defaultValue = "3"; + + sortReflections(arr, ["enum-value-ascending"]); + equal( + arr.map((r) => r.name), + ["a", "c", "b"] + ); + }); + + it("Should sort by enum value descending", () => { + const arr = [ + new DeclarationReflection("a", ReflectionKind.EnumMember), + new DeclarationReflection("b", ReflectionKind.EnumMember), + new DeclarationReflection("c", ReflectionKind.EnumMember), + ]; + arr[0].defaultValue = "123"; + arr[1].defaultValue = "12"; + arr[2].defaultValue = "3"; + + sortReflections(arr, ["enum-value-descending"]); + equal( + arr.map((r) => r.name), + ["a", "b", "c"] + ); + }); + + it("Should not sort enum value descending if not an enum member", () => { + const arr = [ + new DeclarationReflection("c", ReflectionKind.Function), + new DeclarationReflection("a", ReflectionKind.EnumMember), + new DeclarationReflection("b", ReflectionKind.EnumMember), + ]; + arr[0].defaultValue = "123"; + arr[1].defaultValue = "-1"; + arr[2].defaultValue = "3"; + + sortReflections(arr, ["enum-value-descending"]); + equal( + arr.map((r) => r.name), + ["c", "b", "a"] + ); + }); + + it("Should sort by static first", () => { + const arr = [ + new DeclarationReflection("a", ReflectionKind.Function), + new DeclarationReflection("b", ReflectionKind.Function), + new DeclarationReflection("c", ReflectionKind.Function), + ]; + arr[0].setFlag(ReflectionFlag.Static, true); + arr[1].setFlag(ReflectionFlag.Static, false); + arr[2].setFlag(ReflectionFlag.Static, true); + + sortReflections(arr, ["static-first"]); + equal( + arr.map((r) => r.name), + ["a", "c", "b"] + ); + }); + + it("Should sort by instance first", () => { + const arr = [ + new DeclarationReflection("a", ReflectionKind.Function), + new DeclarationReflection("b", ReflectionKind.Function), + new DeclarationReflection("c", ReflectionKind.Function), + ]; + arr[0].setFlag(ReflectionFlag.Static, true); + arr[1].setFlag(ReflectionFlag.Static, false); + arr[2].setFlag(ReflectionFlag.Static, true); + + sortReflections(arr, ["instance-first"]); + equal( + arr.map((r) => r.name), + ["b", "a", "c"] + ); + }); + + it("Should sort by visibility", () => { + const arr = [ + new DeclarationReflection("a", ReflectionKind.Function), + new DeclarationReflection("b", ReflectionKind.Function), + new DeclarationReflection("c", ReflectionKind.Function), + new DeclarationReflection("d", ReflectionKind.Function), + ]; + arr[0].setFlag(ReflectionFlag.Protected, true); + arr[1].setFlag(ReflectionFlag.Private, true); + arr[2].setFlag(ReflectionFlag.Public, true); + // This might not be set. If not set, assumed public. + // arr[3].setFlag(ReflectionFlag.Public, true); + + sortReflections(arr, ["visibility"]); + equal( + arr.map((r) => r.name), + ["c", "d", "a", "b"] + ); + }); + + it("Should sort by required/optional", () => { + const arr = [ + new DeclarationReflection("a", ReflectionKind.Property), + new DeclarationReflection("b", ReflectionKind.Property), + ]; + arr[0].setFlag(ReflectionFlag.Optional, true); + arr[1].setFlag(ReflectionFlag.Optional, false); + + sortReflections(arr, ["required-first"]); + equal( + arr.map((r) => r.name), + ["b", "a"] + ); + }); + + it("Should sort by kind", () => { + const arr = [ + new DeclarationReflection("1", ReflectionKind.Reference), + new DeclarationReflection("25", ReflectionKind.SetSignature), + new DeclarationReflection("3", ReflectionKind.Module), + new DeclarationReflection("4", ReflectionKind.Namespace), + new DeclarationReflection("5", ReflectionKind.Enum), + new DeclarationReflection("6", ReflectionKind.EnumMember), + new DeclarationReflection("16", ReflectionKind.Method), + new DeclarationReflection("8", ReflectionKind.Interface), + new DeclarationReflection("9", ReflectionKind.TypeAlias), + new DeclarationReflection("10", ReflectionKind.Constructor), + new DeclarationReflection("11", ReflectionKind.Event), + new DeclarationReflection("2", ReflectionKind.Project), + new DeclarationReflection("24", ReflectionKind.GetSignature), + new DeclarationReflection("13", ReflectionKind.Variable), + new DeclarationReflection("14", ReflectionKind.Function), + new DeclarationReflection("15", ReflectionKind.Accessor), + new DeclarationReflection("12", ReflectionKind.Property), + new DeclarationReflection("20", ReflectionKind.TypeLiteral), + new DeclarationReflection("17", ReflectionKind.ObjectLiteral), + new DeclarationReflection("18", ReflectionKind.Parameter), + new DeclarationReflection("19", ReflectionKind.TypeParameter), + new DeclarationReflection("21", ReflectionKind.CallSignature), + new DeclarationReflection("7", ReflectionKind.Class), + new DeclarationReflection( + "22", + ReflectionKind.ConstructorSignature + ), + new DeclarationReflection("23", ReflectionKind.IndexSignature), + ]; + + sortReflections(arr, ["kind"]); + equal( + arr.map((r) => r.name), + Array.from({ length: arr.length }, (_, i) => (i + 1).toString()) + ); + }); + + it("Should sort with multiple strategies", () => { + resetReflectionID(); + const arr = [ + new DeclarationReflection("a", ReflectionKind.Function), + new DeclarationReflection("a", ReflectionKind.Function), + new DeclarationReflection("b", ReflectionKind.Function), + new DeclarationReflection("b", ReflectionKind.Function), + ]; + arr[0].setFlag(ReflectionFlag.Optional, true); + arr[2].setFlag(ReflectionFlag.Optional, true); + + sortReflections(arr, ["required-first", "alphabetical"]); + equal( + arr.map((r) => r.id), + [1, 3, 0, 2] + ); + }); + + it("source-order should do nothing if no symbols are available", () => { + const proj = new ProjectReflection(""); + const arr = [ + new DeclarationReflection("b", ReflectionKind.Function, proj), + new DeclarationReflection("a", ReflectionKind.Function, proj), + ]; + + sortReflections(arr, ["source-order", "alphabetical"]); + equal( + arr.map((r) => r.name), + ["a", "b"] + ); + }); +});