diff --git a/CHANGELOG.md b/CHANGELOG.md index e6bd078e6b..bf4f216846 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Fixed default option values on options declared by plugins in packages mode, #2433. - `gitRevision` will now be replaced in `sourceLinkTemplate`, #2434. +- Improved handling of function-modules created with `Object.assign`, #2436. - Fixed an infinite loop when `skipLibCheck` is used to ignore some compiler errors, #2438. ### Thanks! diff --git a/package.json b/package.json index 9b93b9d424..7b8ad3eb65 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "test": "mocha --config .config/mocha.fast.json", "test:cov": "c8 mocha --config .config/mocha.fast.json", "doc:c": "node bin/typedoc --tsconfig src/test/converter/tsconfig.json", - "doc:c2": "node --inspect-brk bin/typedoc --tsconfig src/test/converter2/tsconfig.json", + "doc:c2": "node bin/typedoc --tsconfig src/test/converter2/tsconfig.json", "test:full": "c8 mocha --config .config/mocha.full.json", "test:visual": "ts-node ./src/test/capture-screenshots.ts && ./scripts/compare_screenshots.sh", "test:visual:accept": "node scripts/accept_visual_regression.js", diff --git a/src/lib/converter/symbols.ts b/src/lib/converter/symbols.ts index ab41ae440b..47705c8524 100644 --- a/src/lib/converter/symbols.ts +++ b/src/lib/converter/symbols.ts @@ -178,18 +178,6 @@ export function convertSymbol( flags = removeFlag(flags, ts.SymbolFlags.Property); } - // A default exported function with no associated variable is a property, but - // we should really convert it as a variable for documentation purposes - // export default () => {} - // export default 123 - if ( - flags === ts.SymbolFlags.Property && - symbol.name === "default" && - context.scope.kindOf(ReflectionKind.Module | ReflectionKind.Project) - ) { - flags = ts.SymbolFlags.BlockScopedVariable; - } - for (const flag of getEnumFlags(flags ^ allConverterFlags)) { if (!(flag & allConverterFlags)) { context.logger.verbose( @@ -645,6 +633,14 @@ function convertProperty( symbol: ts.Symbol, exportSymbol?: ts.Symbol, ) { + // This might happen if we're converting a function-module created with Object.assign + // or `export default () => {}` + if ( + context.scope.kindOf(ReflectionKind.SomeModule | ReflectionKind.Project) + ) { + return convertVariable(context, symbol, exportSymbol); + } + const declarations = symbol.getDeclarations() ?? []; // Don't do anything if we inherited this property and it is private. @@ -1019,6 +1015,24 @@ function convertVariableAsFunction( ); } + // #2436: Functions created with Object.assign on a function won't have a namespace flag + // but likely have properties that we should put into a namespace. + if ( + type.getProperties().length && + !hasAnyFlag( + symbol.flags, + ts.SymbolFlags.NamespaceModule | ts.SymbolFlags.ValueModule, + ) + ) { + const ns = context.createDeclarationReflection( + ReflectionKind.Namespace, + symbol, + exportSymbol, + ); + context.finalizeDeclarationReflection(ns); + convertSymbols(context.withScope(ns), type.getProperties()); + } + return ts.SymbolFlags.Property; } diff --git a/src/test/converter2/issues/gh2436.ts b/src/test/converter2/issues/gh2436.ts new file mode 100644 index 0000000000..e5219716b9 --- /dev/null +++ b/src/test/converter2/issues/gh2436.ts @@ -0,0 +1,17 @@ +/** The FOO function */ +function foo() { + return "foo"; +} + +function bugInner(): { foo: string } { + return { foo: "bar" }; +} + +export const bug: { + (): { foo: string }; + foo: typeof foo; + bar: 42; +} = Object.assign(bugInner, { + foo, + bar: 42 as const, +}); diff --git a/src/test/issues.c2.test.ts b/src/test/issues.c2.test.ts index 590d140414..cf6558642c 100644 --- a/src/test/issues.c2.test.ts +++ b/src/test/issues.c2.test.ts @@ -1197,6 +1197,22 @@ describe("Issue Tests", () => { ); }); + it("Handles function-namespaces created with Object.assign #2436", () => { + const project = convert(); + equal(project.children?.map((c) => c.kind), [ + ReflectionKind.Namespace, + ReflectionKind.Function, + ]); + equal( + project.children[0].getChildByName("bar")?.kind, + ReflectionKind.Variable, + ); + equal( + project.children[0].getChildByName("foo")?.kind, + ReflectionKind.Function, + ); + }); + it("Handles recursive aliases without looping infinitely #2438", () => { const bad = query(convert(), "Bad"); equal(bad.kind, ReflectionKind.Interface);