diff --git a/adev/shared-docs/pipeline/api-gen/rendering/test/fake-entries.json b/adev/shared-docs/pipeline/api-gen/rendering/test/fake-entries.json index d5270472877..d5849a04e6a 100644 --- a/adev/shared-docs/pipeline/api-gen/rendering/test/fake-entries.json +++ b/adev/shared-docs/pipeline/api-gen/rendering/test/fake-entries.json @@ -754,6 +754,525 @@ "startLine": 112, "endLine": 115 } + }, + { + "name": "HttpRequest", + "isAbstract": false, + "entryType": "undecorated_class", + "members": [ + { + "name": "constructor", + "signatures": [ + { + "name": "constructor", + "params": [ + { + "name": "method", + "description": "", + "type": "\"GET\" | \"HEAD\"", + "isOptional": false, + "isRestParam": false + }, + { + "name": "url", + "description": "", + "type": "string", + "isOptional": false, + "isRestParam": false + }, + { + "name": "init", + "description": "", + "type": "{ headers?: HttpHeaders | undefined; context?: HttpContext | undefined; reportProgress?: boolean | undefined; params?: HttpParams | undefined; responseType?: \"arraybuffer\" | \"blob\" | \"text\" | \"json\" | undefined; withCredentials?: boolean | undefined; transferCache?: boolean | { includeHeaders?: string[] | undefined; } | undefined; } | undefined", + "isOptional": true, + "isRestParam": false + } + ], + "returnType": "foobar", + "description": "", + "entryType": "function", + "jsdocTags": [], + "rawComment": "", + "generics": [], + "isNewType": false + }, + { + "name": "constructor", + "params": [ + { + "name": "method", + "description": "", + "type": "\"DELETE\" | \"JSONP\" | \"OPTIONS\"", + "isOptional": false, + "isRestParam": false + }, + { + "name": "url", + "description": "", + "type": "string", + "isOptional": false, + "isRestParam": false + }, + { + "name": "init", + "description": "", + "type": "{ headers?: HttpHeaders | undefined; context?: HttpContext | undefined; reportProgress?: boolean | undefined; params?: HttpParams | undefined; responseType?: \"arraybuffer\" | \"blob\" | \"text\" | \"json\" | undefined; withCredentials?: boolean | undefined; } | undefined", + "isOptional": true, + "isRestParam": false + } + ], + "returnType": "foobar", + "description": "", + "entryType": "function", + "jsdocTags": [], + "rawComment": "", + "generics": [], + "isNewType": false + }, + { + "name": "constructor", + "params": [ + { + "name": "method", + "description": "", + "type": "\"POST\"", + "isOptional": false, + "isRestParam": false + }, + { + "name": "url", + "description": "", + "type": "string", + "isOptional": false, + "isRestParam": false + }, + { + "name": "body", + "description": "", + "type": "T | null", + "isOptional": false, + "isRestParam": false + }, + { + "name": "init", + "description": "", + "type": "{ headers?: HttpHeaders | undefined; context?: HttpContext | undefined; reportProgress?: boolean | undefined; params?: HttpParams | undefined; responseType?: \"arraybuffer\" | \"blob\" | \"text\" | \"json\" | undefined; withCredentials?: boolean | undefined; transferCache?: boolean | { includeHeaders?: string[] | undefined; } | undefined; } | undefined", + "isOptional": true, + "isRestParam": false + } + ], + "returnType": "foobar", + "description": "", + "entryType": "function", + "jsdocTags": [], + "rawComment": "", + "generics": [], + "isNewType": false + }, + { + "name": "constructor", + "params": [ + { + "name": "method", + "description": "", + "type": "\"PUT\" | \"PATCH\"", + "isOptional": false, + "isRestParam": false + }, + { + "name": "url", + "description": "", + "type": "string", + "isOptional": false, + "isRestParam": false + }, + { + "name": "body", + "description": "", + "type": "T | null", + "isOptional": false, + "isRestParam": false + }, + { + "name": "init", + "description": "", + "type": "{ headers?: HttpHeaders | undefined; context?: HttpContext | undefined; reportProgress?: boolean | undefined; params?: HttpParams | undefined; responseType?: \"arraybuffer\" | \"blob\" | \"text\" | \"json\" | undefined; withCredentials?: boolean | undefined; } | undefined", + "isOptional": true, + "isRestParam": false + } + ], + "returnType": "foobar", + "description": "", + "entryType": "function", + "jsdocTags": [], + "rawComment": "", + "generics": [], + "isNewType": false + }, + { + "name": "constructor", + "params": [ + { + "name": "method", + "description": "", + "type": "string", + "isOptional": false, + "isRestParam": false + }, + { + "name": "url", + "description": "", + "type": "string", + "isOptional": false, + "isRestParam": false + }, + { + "name": "body", + "description": "", + "type": "T | null", + "isOptional": false, + "isRestParam": false + }, + { + "name": "init", + "description": "", + "type": "{ headers?: HttpHeaders | undefined; context?: HttpContext | undefined; reportProgress?: boolean | undefined; params?: HttpParams | undefined; responseType?: \"arraybuffer\" | \"blob\" | \"text\" | \"json\" | undefined; withCredentials?: boolean | undefined; transferCache?: boolean | { includeHeaders?: string[] | undefined; } | undefined; } | undefined", + "isOptional": true, + "isRestParam": false + } + ], + "returnType": "foobar", + "description": "", + "entryType": "function", + "jsdocTags": [], + "rawComment": "", + "generics": [], + "isNewType": false + }, + { + "name": "constructor", + "params": [ + { + "name": "method", + "description": "", + "type": "string", + "isOptional": false, + "isRestParam": false + }, + { + "name": "url", + "description": "", + "type": "string", + "isOptional": false, + "isRestParam": false + }, + { + "name": "third", + "description": "", + "type": "T | { headers?: HttpHeaders | undefined; context?: HttpContext | undefined; reportProgress?: boolean | undefined; params?: HttpParams | undefined; responseType?: \"arraybuffer\" | \"blob\" | \"text\" | \"json\" | undefined; withCredentials?: boolean | undefined; transferCache?: boolean | { includeHeaders?: string[] | undefined; } | undefined; } | null | undefined", + "isOptional": true, + "isRestParam": false + }, + { + "name": "fourth", + "description": "", + "type": "{ headers?: HttpHeaders | undefined; context?: HttpContext | undefined; reportProgress?: boolean | undefined; params?: HttpParams | undefined; responseType?: \"arraybuffer\" | \"blob\" | \"text\" | \"json\" | undefined; withCredentials?: boolean | undefined; transferCache?: boolean | { includeHeaders?: string[] | undefined; } | undefined; } | undefined", + "isOptional": true, + "isRestParam": false + } + ], + "returnType": "foobar", + "description": "", + "entryType": "function", + "jsdocTags": [], + "rawComment": "", + "generics": [], + "isNewType": false + } + ], + "implementation": { + "params": [ + { + "name": "method", + "description": "", + "type": "string", + "isOptional": false, + "isRestParam": false + }, + { + "name": "url", + "description": "", + "type": "string", + "isOptional": false, + "isRestParam": false + }, + { + "name": "third", + "description": "", + "type": "T | { headers?: HttpHeaders | undefined; context?: HttpContext | undefined; reportProgress?: boolean | undefined; params?: HttpParams | undefined; responseType?: \"arraybuffer\" | \"blob\" | \"text\" | \"json\" | undefined; withCredentials?: boolean | undefined; transferCache?: boolean | { includeHeaders?: string[] | undefined; } | undefined; } | null | undefined", + "isOptional": true, + "isRestParam": false + }, + { + "name": "fourth", + "description": "", + "type": "{ headers?: HttpHeaders | undefined; context?: HttpContext | undefined; reportProgress?: boolean | undefined; params?: HttpParams | undefined; responseType?: \"arraybuffer\" | \"blob\" | \"text\" | \"json\" | undefined; withCredentials?: boolean | undefined; transferCache?: boolean | { includeHeaders?: string[] | undefined; } | undefined; } | undefined", + "isOptional": true, + "isRestParam": false + } + ], + "isNewType": false, + "returnType": "HttpRequest", + "generics": [], + "name": "constructor", + "description": "", + "entryType": "function", + "jsdocTags": [], + "rawComment": "" + }, + "entryType": "function", + "description": "", + "jsdocTags": [], + "rawComment": "", + "memberType": "method", + "memberTags": [] + }, + { + "name": "body", + "type": "T | null", + "memberType": "property", + "memberTags": ["readonly"], + "description": "The request body, or `null` if one isn't set.\n\nBodies are not enforced to be immutable, as they can include a reference to any\nuser-defined data type. However, interceptors should take care to preserve\nidempotence by treating them as such.", + "jsdocTags": [] + }, + { + "name": "headers", + "type": "HttpHeaders", + "memberType": "property", + "memberTags": ["readonly"], + "description": "Outgoing headers for this request.", + "jsdocTags": [] + }, + { + "name": "context", + "type": "HttpContext", + "memberType": "property", + "memberTags": ["readonly"], + "description": "Shared and mutable context that can be used by interceptors", + "jsdocTags": [] + }, + { + "name": "reportProgress", + "type": "boolean", + "memberType": "property", + "memberTags": ["readonly"], + "description": "Whether this request should be made in a way that exposes progress events.\n\nProgress events are expensive (change detection runs on each event) and so\nthey should only be requested if the consumer intends to monitor them.\n\nNote: The `FetchBackend` doesn't support progress report on uploads.", + "jsdocTags": [] + }, + { + "name": "withCredentials", + "type": "boolean", + "memberType": "property", + "memberTags": ["readonly"], + "description": "Whether this request should be sent with outgoing credentials (cookies).", + "jsdocTags": [] + }, + { + "name": "responseType", + "type": "\"arraybuffer\" | \"blob\" | \"text\" | \"json\"", + "memberType": "property", + "memberTags": ["readonly"], + "description": "The expected response type of the server.\n\nThis is used to parse the response appropriately before returning it to\nthe requestee.", + "jsdocTags": [] + }, + { + "name": "method", + "type": "string", + "memberType": "property", + "memberTags": ["readonly"], + "description": "The outgoing HTTP request method.", + "jsdocTags": [] + }, + { + "name": "params", + "type": "HttpParams", + "memberType": "property", + "memberTags": ["readonly"], + "description": "Outgoing URL parameters.\n\nTo pass a string representation of HTTP parameters in the URL-query-string format,\nthe `HttpParamsOptions`' `fromString` may be used. For example:\n\n```ts\nnew HttpParams({fromString: 'angular=awesome'})\n```", + "jsdocTags": [] + }, + { + "name": "urlWithParams", + "type": "string", + "memberType": "property", + "memberTags": ["readonly"], + "description": "The outgoing URL with all URL parameters set.", + "jsdocTags": [] + }, + { + "name": "transferCache", + "type": "boolean | { includeHeaders?: string[] | undefined; } | undefined", + "memberType": "property", + "memberTags": ["readonly", "optional"], + "description": "The HttpTransferCache option for the request", + "jsdocTags": [] + }, + { + "name": "serializeBody", + "signatures": [ + { + "name": "serializeBody", + "entryType": "function", + "description": "Transform the free-form body into a serialized format suitable for\ntransmission to the server.", + "generics": [], + "isNewType": false, + "jsdocTags": [], + "params": [], + "rawComment": "/**\n * Transform the free-form body into a serialized format suitable for\n * transmission to the server.\n */", + "returnType": "string | ArrayBuffer | Blob | FormData | URLSearchParams | null" + } + ], + "implementation": { + "params": [], + "isNewType": false, + "returnType": "string | ArrayBuffer | Blob | FormData | URLSearchParams | null", + "generics": [], + "name": "serializeBody", + "description": "Transform the free-form body into a serialized format suitable for\ntransmission to the server.", + "entryType": "function", + "jsdocTags": [], + "rawComment": "/**\n * Transform the free-form body into a serialized format suitable for\n * transmission to the server.\n */" + }, + "entryType": "function", + "description": "Transform the free-form body into a serialized format suitable for\ntransmission to the server.", + "jsdocTags": [], + "rawComment": "/**\n * Transform the free-form body into a serialized format suitable for\n * transmission to the server.\n */", + "memberType": "method", + "memberTags": [] + }, + { + "name": "detectContentTypeHeader", + "signatures": [ + { + "name": "detectContentTypeHeader", + "entryType": "function", + "description": "Examine the body and attempt to infer an appropriate MIME type\nfor it.\n\nIf no such type can be inferred, this method will return `null`.", + "generics": [], + "isNewType": false, + "jsdocTags": [], + "params": [], + "rawComment": "/**\n * Examine the body and attempt to infer an appropriate MIME type\n * for it.\n *\n * If no such type can be inferred, this method will return `null`.\n */", + "returnType": "string | null" + } + ], + "implementation": { + "params": [], + "isNewType": false, + "returnType": "string | null", + "generics": [], + "name": "detectContentTypeHeader", + "description": "Examine the body and attempt to infer an appropriate MIME type\nfor it.\n\nIf no such type can be inferred, this method will return `null`.", + "entryType": "function", + "jsdocTags": [], + "rawComment": "/**\n * Examine the body and attempt to infer an appropriate MIME type\n * for it.\n *\n * If no such type can be inferred, this method will return `null`.\n */" + }, + "entryType": "function", + "description": "Examine the body and attempt to infer an appropriate MIME type\nfor it.\n\nIf no such type can be inferred, this method will return `null`.", + "jsdocTags": [], + "rawComment": "/**\n * Examine the body and attempt to infer an appropriate MIME type\n * for it.\n *\n * If no such type can be inferred, this method will return `null`.\n */", + "memberType": "method", + "memberTags": [] + }, + { + "name": "clone", + "signatures": [ + { + "name": "clone", + "entryType": "function", + "description": "", + "generics": [], + "isNewType": false, + "jsdocTags": [], + "params": [], + "rawComment": "", + "returnType": "HttpRequest" + }, + { + "name": "clone", + "entryType": "function", + "description": "", + "generics": [], + "isNewType": false, + "jsdocTags": [], + "params": [ + { + "name": "update", + "description": "", + "type": "{ headers?: HttpHeaders | undefined; context?: HttpContext | undefined; reportProgress?: boolean | undefined; params?: HttpParams | undefined; responseType?: \"arraybuffer\" | \"blob\" | \"text\" | \"json\" | undefined; withCredentials?: boolean | undefined; transferCache?: boolean | { includeHeaders?: string[] | undefined; } | undefined; body?: T | null | undefined; method?: string | undefined; url?: string | undefined; setHeaders?: { [name: string]: string | string[]; } | undefined; setParams?: { [param: string]: string; } | undefined; }", + "isOptional": false, + "isRestParam": false + } + ], + "rawComment": "", + "returnType": "HttpRequest" + }, + { + "name": "clone", + "entryType": "function", + "description": "", + "generics": [{"name": "V"}], + "isNewType": false, + "jsdocTags": [], + "params": [ + { + "name": "update", + "description": "", + "type": "{ headers?: HttpHeaders | undefined; context?: HttpContext | undefined; reportProgress?: boolean | undefined; params?: HttpParams | undefined; responseType?: \"arraybuffer\" | \"blob\" | \"text\" | \"json\" | undefined; withCredentials?: boolean | undefined; transferCache?: boolean | { includeHeaders?: string[] | undefined; } | undefined; body?: V | null | undefined; method?: string | undefined; url?: string | undefined; setHeaders?: { [name: string]: string | string[]; } | undefined; setParams?: { [param: string]: string; } | undefined; }", + "isOptional": false, + "isRestParam": false + } + ], + "rawComment": "", + "returnType": "HttpRequest" + } + ], + "implementation": { + "params": [ + { + "name": "update", + "description": "", + "type": "{ headers?: HttpHeaders | undefined; context?: HttpContext | undefined; reportProgress?: boolean | undefined; params?: HttpParams | undefined; responseType?: \"arraybuffer\" | \"blob\" | \"text\" | \"json\" | undefined; withCredentials?: boolean | undefined; transferCache?: boolean | { includeHeaders?: string[] | undefined; } | undefined; body?: any; method?: string | undefined; url?: string | undefined; setHeaders?: { [name: string]: string | string[]; } | undefined; setParams?: { [param: string]: string; } | undefined; }", + "isOptional": true, + "isRestParam": false + } + ], + "isNewType": false, + "returnType": "HttpRequest", + "generics": [], + "name": "clone", + "description": "", + "entryType": "function", + "jsdocTags": [], + "rawComment": "" + }, + "entryType": "function", + "description": "", + "jsdocTags": [], + "rawComment": "", + "memberType": "method", + "memberTags": [] + } + ], + "generics": [{"name": "T"}], + "description": "An outgoing HTTP request with an optional typed body.\n\n`HttpRequest` represents an outgoing request, including URL, method,\nheaders, body, and other request configuration options. Instances should be\nassumed to be immutable. To modify a `HttpRequest`, the `clone`\nmethod should be used.", + "jsdocTags": [{"name": "publicApi", "comment": ""}], + "rawComment": "/**\n * An outgoing HTTP request with an optional typed body.\n *\n * `HttpRequest` represents an outgoing request, including URL, method,\n * headers, body, and other request configuration options. Instances should be\n * assumed to be immutable. To modify a `HttpRequest`, the `clone`\n * method should be used.\n *\n * @publicApi\n */", + "implements": [], + "source": { + "filePath": "/packages/common/http/src/request.ts", + "startLine": 131, + "endLine": 578 + } } ], "symbols": [ diff --git a/packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts index 162086d568d..0ad4a65c2a1 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts @@ -117,6 +117,8 @@ class ClassExtractor { return this.extractClassProperty(memberDeclaration); } else if (ts.isAccessor(memberDeclaration)) { return this.extractGetterSetter(memberDeclaration); + } else if (ts.isConstructorDeclaration(memberDeclaration)) { + return this.extractConstructor(memberDeclaration); } // We only expect methods, properties, and accessors. If we encounter something else, @@ -180,6 +182,19 @@ class ClassExtractor { }; } + protected extractConstructor(constructorDeclaration: ts.ConstructorDeclaration): MethodEntry { + const functionExtractor = new FunctionExtractor( + 'constructor', + constructorDeclaration, + this.typeChecker, + ); + return { + ...functionExtractor.extract(), + memberType: MemberType.Method, + memberTags: this.getMemberTags(constructorDeclaration), + }; + } + protected extractInheritance( declaration: ClassDeclaration & ClassDeclarationLike, ): string | undefined { @@ -211,7 +226,9 @@ class ClassExtractor { } /** Gets the tags for a member (protected, readonly, static, etc.) */ - protected getMemberTags(member: MethodLike | PropertyLike): MemberTags[] { + protected getMemberTags( + member: MethodLike | PropertyLike | ts.ConstructorDeclaration, + ): MemberTags[] { const tags: MemberTags[] = this.getMemberTagsFromModifiers(member.modifiers ?? []); if (member.questionToken) { @@ -248,6 +265,7 @@ class ClassExtractor { // classes may narrow types or add method overloads. const type = this.typeChecker.getTypeAtLocation(this.declaration); const members = type.getProperties(); + const constructor = type.getSymbol()?.members?.get(ts.InternalSymbolName.Constructor); // While the properties of the declaration type represent the properties that exist // on a class *instance*, static members are properties on the class symbol itself. @@ -255,7 +273,7 @@ class ClassExtractor { const staticMembers = typeOfConstructor.getProperties(); const result: MemberElement[] = []; - for (const member of [...members, ...staticMembers]) { + for (const member of [...(constructor ? [constructor] : []), ...members, ...staticMembers]) { // A member may have multiple declarations in the case of function overloads. const memberDeclarations = this.filterMethodOverloads(member.getDeclarations() ?? []); for (const memberDeclaration of memberDeclarations) { @@ -272,17 +290,23 @@ class ClassExtractor { private filterMethodOverloads(declarations: ts.Declaration[]): ts.Declaration[] { return declarations.filter((declaration, index) => { // Check if the declaration is a function or method - if (ts.isFunctionDeclaration(declaration) || ts.isMethodDeclaration(declaration)) { + if ( + ts.isFunctionDeclaration(declaration) || + ts.isMethodDeclaration(declaration) || + ts.isConstructorDeclaration(declaration) + ) { // TypeScript ensures that all declarations for a given abstract method appear consecutively. const nextDeclaration = declarations[index + 1]; - const isNextAbstractMethodWithSameName = + const isNextMethodWithSameName = nextDeclaration && - ts.isMethodDeclaration(nextDeclaration) && - nextDeclaration.name.getText() === declaration.name?.getText(); + ((ts.isMethodDeclaration(nextDeclaration) && + nextDeclaration.name.getText() === declaration.name?.getText()) || + (ts.isConstructorDeclaration(nextDeclaration) && + ts.isConstructorDeclaration(declaration))); - // Return only the last occurrence of an abstract method to avoid overload duplication. + // Return only the last occurrence of a method to avoid overload duplication. // Subsequent overloads or implementations are handled separately by the function extractor. - return !isNextAbstractMethodWithSameName; + return !isNextMethodWithSameName; } // Include non-method declarations, such as properties, without filtering. @@ -326,6 +350,11 @@ class ClassExtractor { * - The member is marked as internal via JSDoc. */ private isMemberExcluded(member: MemberElement): boolean { + if (ts.isConstructorDeclaration(member)) { + // A constructor has no name + return false; + } + return ( !member.name || !this.isDocumentableMember(member) || @@ -345,6 +374,7 @@ class ClassExtractor { this.isMethod(member) || this.isProperty(member) || ts.isAccessor(member) || + ts.isConstructorDeclaration(member) || // Signatures are documentable if they are part of an interface. ts.isCallSignatureDeclaration(member) ); diff --git a/packages/compiler-cli/src/ngtsc/docs/src/function_extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/function_extractor.ts index 725dd908c08..a6d3836bbbc 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/function_extractor.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/function_extractor.ts @@ -8,7 +8,7 @@ import ts from 'typescript'; -import {EntryType, FunctionEntry, ParameterEntry} from './entities'; +import {EntryType, FunctionEntry, FunctionSignatureMetadata, ParameterEntry} from './entities'; import {extractGenerics} from './generics_extractor'; import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from './jsdoc_extractor'; import {extractResolvedTypeString} from './type_extractor'; @@ -18,7 +18,8 @@ export type FunctionLike = | ts.MethodDeclaration | ts.MethodSignature | ts.CallSignatureDeclaration - | ts.ConstructSignatureDeclaration; + | ts.ConstructSignatureDeclaration + | ts.ConstructorDeclaration; export class FunctionExtractor { constructor( @@ -45,7 +46,9 @@ export class FunctionExtractor { this.exportDeclaration; const type = this.typeChecker.getTypeAtLocation(this.exportDeclaration); - const overloads = extractCallSignatures(this.name, this.typeChecker, type); + const overloads = ts.isConstructorDeclaration(this.exportDeclaration) + ? constructorOverloads(this.exportDeclaration, this.typeChecker) + : extractCallSignatures(this.name, this.typeChecker, type); const jsdocsTags = extractJsDocTags(implementation); const description = extractJsDocDescription(implementation); @@ -72,6 +75,32 @@ export class FunctionExtractor { } } +function constructorOverloads( + constructorDeclaration: ts.ConstructorDeclaration, + typeChecker: ts.TypeChecker, +) { + const classDeclaration = constructorDeclaration.parent; + const constructorNode = classDeclaration.members.filter( + (member): member is ts.ConstructorDeclaration => { + return ts.isConstructorDeclaration(member) && !member.body; + }, + ); + + return constructorNode.map((n): FunctionSignatureMetadata => { + return { + name: 'constructor', + params: extractAllParams(n.parameters, typeChecker), + returnType: typeChecker.getTypeAtLocation(classDeclaration)?.symbol.name, + description: extractJsDocDescription(n), + entryType: EntryType.Function, + jsdocTags: extractJsDocTags(n), + rawComment: extractRawJsDoc(n), + generics: extractGenerics(n), + isNewType: false, + }; + }); +} + /** Extracts parameters of the given parameter declaration AST nodes. */ export function extractAllParams( params: ts.NodeArray, @@ -90,14 +119,19 @@ export function extractAllParams( function filterSignatureDeclarations(signatures: readonly ts.Signature[]) { const result: Array<{ signature: ts.Signature; - decl: ts.FunctionDeclaration | ts.CallSignatureDeclaration | ts.MethodDeclaration; + decl: + | ts.FunctionDeclaration + | ts.CallSignatureDeclaration + | ts.MethodDeclaration + | ts.ConstructSignatureDeclaration; }> = []; for (const signature of signatures) { const decl = signature.getDeclaration(); if ( ts.isFunctionDeclaration(decl) || ts.isCallSignatureDeclaration(decl) || - ts.isMethodDeclaration(decl) + ts.isMethodDeclaration(decl) || + ts.isConstructSignatureDeclaration(decl) ) { result.push({signature, decl}); } diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/class_doc_extraction_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/class_doc_extraction_spec.ts index c47316c9f3d..ee600393fe8 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/class_doc_extraction_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/class_doc_extraction_spec.ts @@ -55,6 +55,61 @@ runInEachFileSystem(() => { expect(customSliderEntry.entryType).toBe(EntryType.UndecoratedClass); }); + it('should extract class constructor', () => { + env.write( + 'index.ts', + ` + export class UserProfile { + constructor(foo: number) {} + } + `, + ); + + const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); + const classEntry = docs[0] as ClassEntry; + expect(classEntry.members.length).toBe(1); + + const methodEntry = classEntry.members[0] as MethodEntry; + expect(methodEntry.memberType).toBe(MemberType.Method); + expect(methodEntry.name).toBe('constructor'); + expect(methodEntry.implementation.params.length).toBe(1); + expect(methodEntry.implementation.params[0].name).toBe('foo'); + expect(methodEntry.implementation.params[0].type).toBe('number'); + }); + + it('should extract class constructor with overloads', () => { + env.write( + 'index.ts', + ` + export class UserProfile { + constructor(value: boolean); + constructor(value: number); + constructor(value: number | boolean | string) { } + } + `, + ); + + const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); + const classEntry = docs[0] as ClassEntry; + expect(classEntry.members.length).toBe(1); + + const constructorEntry = classEntry.members[0] as MethodEntry; + + expect(constructorEntry.signatures.length).toBe(2); + + const [booleanOverloadEntry, numberOverloadEntry] = constructorEntry.signatures; + + expect(booleanOverloadEntry.name).toBe('constructor'); + expect(booleanOverloadEntry.params.length).toBe(1); + expect(booleanOverloadEntry.params[0].type).toBe('boolean'); + expect(booleanOverloadEntry.returnType).toBe('UserProfile'); + + expect(numberOverloadEntry.name).toBe('constructor'); + expect(numberOverloadEntry.params.length).toBe(1); + expect(numberOverloadEntry.params[0].type).toBe('number'); + expect(numberOverloadEntry.returnType).toBe('UserProfile'); + }); + it('should extract class members', () => { env.write( 'index.ts', @@ -650,9 +705,14 @@ runInEachFileSystem(() => { expect(docs.length).toBe(1); const classEntry = docs[0] as ClassEntry; - expect(classEntry.members.length).toBe(2); - const [myPropEntry, fooEntry] = classEntry.members as PropertyEntry[]; + // constructor, myProp, foo (others are non-public) + expect(classEntry.members.length).toBe(3); + + const [constructorEntry, myPropEntry, fooEntry] = classEntry.members as PropertyEntry[]; + + expect(constructorEntry.name).toBe('constructor'); + expect(constructorEntry.memberType).toBe(MemberType.Method); expect(myPropEntry.name).toBe('myProp'); expect(myPropEntry.memberType).toBe(MemberType.Property);