Skip to content
This repository was archived by the owner on Mar 31, 2025. It is now read-only.

fix(typescript): handle property accessors correctly #247

Merged
merged 1 commit into from
Oct 8, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions typescript/mocks/readTypeScriptModules/gettersAndSetters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export class Test {
get foo1(): string { return null; }
/** foo1 setter */
set foo1(value: string) { /* do nothing */}

set foo2(value: string) { /* do nothing */}
/** foo2 getter */
get foo2(): string { return null; }

/** bar getter */
get bar(): number { return null; }

/** qux setter */
set qux(value: object) { /* do nothing */ }

/** This has no explicit getter return type */
get noType() { return 'x'; }
/** So we get it from the setter instead */
set noType(value: string) { /**/ }
}
22 changes: 22 additions & 0 deletions typescript/src/api-doc-types/AccessorInfoDoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Declaration, SignatureDeclaration, TypeChecker } from 'typescript';
import { getDeclarationTypeText } from '../services/TsParser/getDeclarationTypeText';
import { getParameters } from '../services/TsParser/getParameters';
import { encodeAnchor } from '../utils/encodeAnchor';
import { MethodMemberDoc } from './MethodMemberDoc';
import { PropertyMemberDoc } from './PropertyMemberDoc';

/**
* This represents a getter or setter overload of an accessor property.
* There will be a PropertyMemberDoc that contains these docs.
*/
export class AccessorInfoDoc extends MethodMemberDoc {
docType = `${this.accessorType}-accessor-info`;
name = `${this.propertyDoc.name}:${this.accessorType}`;
id = `${this.propertyDoc.id}:${this.accessorType}`;
aliases = this.propertyDoc.aliases.map(alias => `${alias}:${this.accessorType}`);
anchor = encodeAnchor(this.name);

constructor(public accessorType: 'get'|'set', public propertyDoc: PropertyMemberDoc, declaration: Declaration, basePath: string, namespacesToInclude: string[]) {
super(propertyDoc.containerDoc, propertyDoc.symbol, declaration, basePath, namespacesToInclude, false);
}
}
19 changes: 16 additions & 3 deletions typescript/src/api-doc-types/ContainerExportDoc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* tslint:disable:no-bitwise */
import { FunctionLikeDeclaration, Map, Symbol, SymbolFlags } from 'typescript';
import { FunctionLikeDeclaration, GetAccessorDeclaration, Map, SetAccessorDeclaration, Symbol, SymbolFlags, SyntaxKind } from 'typescript';
import { FileInfo } from "../services/TsParser/FileInfo";
import { getAccessibility } from "../services/TsParser/getAccessibility";
import { AccessorInfoDoc } from './AccessorInfoDoc';
import { ExportDoc } from './ExportDoc' ;
import { MemberDoc } from './MemberDoc' ;
import { MethodMemberDoc } from './MethodMemberDoc';
Expand Down Expand Up @@ -38,22 +39,34 @@ export abstract class ContainerExportDoc extends ExportDoc {

const overloads: MethodMemberDoc[] = [];
let memberDoc: MemberDoc|null = null;
let getAccessorDeclaration: GetAccessorDeclaration | null = null;
let setAccessorDeclaration: SetAccessorDeclaration | null = null;

for (const declaration of member.getDeclarations()!) {
if (flags & MethodMemberFlags) {
if ((declaration as FunctionLikeDeclaration).body) {
if (declaration.kind === SyntaxKind.GetAccessor) {
getAccessorDeclaration = declaration as GetAccessorDeclaration;
} else if (declaration.kind === SyntaxKind.SetAccessor) {
setAccessorDeclaration = declaration as SetAccessorDeclaration;
} else if ((declaration as FunctionLikeDeclaration).body) {
// This is the "real" declaration of the method
memberDoc = new MethodMemberDoc(this, member, declaration, this.basePath, this.namespacesToInclude, isStatic, overloads);
} else {
// This is an overload signature of the method
overloads.push(new MethodMemberDoc(this, member, declaration, this.basePath, this.namespacesToInclude, isStatic, overloads));
}
} else if (flags & PropertyMemberFlags) {
memberDoc = new PropertyMemberDoc(this, member, declaration, this.basePath, this.namespacesToInclude, isStatic);
memberDoc = new PropertyMemberDoc(this, member, declaration, null, null, this.basePath, this.namespacesToInclude, isStatic);
} else {
throw new Error(`Unknown member type for member ${member.name}`);
}
}

// If at least one of the declarations was an accessor then the whole member is a property.
if (getAccessorDeclaration || setAccessorDeclaration) {
memberDoc = new PropertyMemberDoc(this, member, null, getAccessorDeclaration, setAccessorDeclaration, this.basePath, this.namespacesToInclude, false);
}

// If there is no member doc then we are in an interface or abstract class and we just take the first overload
// as the primary one.
memberDocs.push(memberDoc || overloads.shift()!);
Expand Down
26 changes: 26 additions & 0 deletions typescript/src/api-doc-types/PropertyMemberDoc.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,35 @@
import { Declaration, GetAccessorDeclaration, SetAccessorDeclaration, SignatureDeclaration, Symbol, SyntaxKind } from 'typescript';
import { getDeclarationTypeText } from "../services/TsParser/getDeclarationTypeText";
import { getParameters } from '../services/TsParser/getParameters';
import { encodeAnchor } from '../utils/encodeAnchor';
import { AccessorInfoDoc } from './AccessorInfoDoc';
import { ContainerExportDoc } from './ContainerExportDoc';
import { MemberDoc } from './MemberDoc';

export class PropertyMemberDoc extends MemberDoc {
name = this.symbol.name;
anchor = encodeAnchor(this.name);
id = `${this.containerDoc.id}.${this.name}`;
aliases = this.containerDoc.aliases.map(alias => `${alias}.${this.name}` );
getAccessor: AccessorInfoDoc | null;
setAccessor: AccessorInfoDoc | null;

constructor(
containerDoc: ContainerExportDoc,
symbol: Symbol,
declaration: Declaration | null,
getAccessorDeclaration: GetAccessorDeclaration | null,
setAccessorDeclaration: SetAccessorDeclaration | null,
basePath: string,
namespacesToInclude: string[],
isStatic: boolean,
) {
super(containerDoc, symbol, (declaration || getAccessorDeclaration || setAccessorDeclaration)!, basePath, namespacesToInclude, isStatic);

// If this property has accessors then compute the type based on that instead
this.getAccessor = getAccessorDeclaration && new AccessorInfoDoc('get', this, getAccessorDeclaration, basePath, namespacesToInclude);
this.setAccessor = setAccessorDeclaration && new AccessorInfoDoc('set', this, setAccessorDeclaration, basePath, namespacesToInclude);
this.type = this.type || this.setAccessor && this.setAccessor.parameters[0].split(/:\s?/)[1] || '';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have passed in declaration or getAccessorDeclaration then we will have extracted the type from one of those, if possible.

Copy link
Member

@devversion devversion Oct 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's true. On Material we have a small workaround for this, but it still uses the declaration to retrieve the type. See here

Copy link
Member

@devversion devversion Oct 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth to also check how the description of the getter/setter is being handled. I noticed that the description is only being read from one place.

get test() {}

/** Description on the setter */
set test(val: boolean) {}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the getter and setter are docs in their own right, so their documentation should be processed for jsdoc tags, etc. You can then access them via the member.getAccessor and member.setAccessor for use in the property template.

This is similar to how overloads are being dealt with. With overloads we extract the content of the "actual" function for the member doc but then attach the content for each of the overload declarations to the overloads for that member.

In the case of a property with a getter and setter, we extract the content from the first declaration that is defined from [declaration, getter, setter]. It would seem to me more common to put the documentation (if there is only one) on the getter not the setter YMMV. But if you do put it only on the setter then you could easily get around this by doing something like:

Something like:

<h2 class="property-name">{$ member.name $}</h2>
<p class="description">{$ member.content or member.setAccessor and member.getAccessor.content $}</p>

Is this too unreasonable? Do you really put documentation on the setter only?

Copy link
Member

@devversion devversion Oct 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really, on Material I remember seeing accessors that have the descriptions on a setter or on the getter and I want to make sure we don't need to enforce something here.

For the description, for me it makes sense to have a description property on the "MemberDoc". The description there should be retrieved in a similar way as the type, because it can be either on the getter or setter.

// In theory the accessor should only have one JSDoc at all, 
// it's still considered a property.
memberDoc.description // Should show the proper description of the getter/setter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I'll transfer that from the relevant accessor too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done PTAL

Copy link
Member

@devversion devversion Oct 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tested it locally and the types seem to work fine now. For the descriptions, I'm not sure what's different to content is? When debugging most of our accessors in Material, they don't seem to have a content either.

Isn't there a way to set the description as well, based on the two cases I described above?

    // This works fine now. Seems like every thing has a type now!
    if (!propertyDoc.type) {
      console.log('No type for', propertyDoc.name);
    }

    // Likely every accessor doesn't have a description. 
    // "content" seems to be empty in most of the cases too.
    if (!propertyDoc.description) {
      console.log('No description for', propertyDoc.name, propertyDoc.content);
    }

This is the code-snippet we currently use in Material to receive the proper description for an accessor.

https://github.com/angular/material2/blob/4d97271786855598cfd18921641d5ffaafa079d8/tools/dgeni/common/dgeni-accessors-parse.ts#L51-L54

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember that this processor runs before the jsdic processing happens. The content field contains the raw content. After jsdoc runs there will be a description property too

Copy link
Member

@devversion devversion Oct 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I'm not sure why the content is empty then. For me likely every accessor document has a content that is empty. Discussed on Slack.

this.content = this.content || this.setAccessor && this.setAccessor.content || '';
}
}
109 changes: 109 additions & 0 deletions typescript/src/processors/readTypeScriptModules/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import {Dgeni, DocCollection} from 'dgeni';
import {Injector} from 'dgeni/lib/Injector';
import { Symbol } from 'typescript';
import {ReadTypeScriptModules} from '.';
import {AccessorInfoDoc} from '../../api-doc-types/AccessorInfoDoc';
import {ClassExportDoc} from '../../api-doc-types/ClassExportDoc';
import {ExportDoc} from '../../api-doc-types/ExportDoc';
import {InterfaceExportDoc} from '../../api-doc-types/InterfaceExportDoc';
import {MethodMemberDoc} from '../../api-doc-types/MethodMemberDoc';
import {ModuleDoc} from '../../api-doc-types/ModuleDoc';
import {PropertyMemberDoc} from '../../api-doc-types/PropertyMemberDoc';
const mockPackage = require('../../mocks/mockPackage');
const path = require('canonical-path');

Expand Down Expand Up @@ -336,6 +338,113 @@ describe('readTypeScriptModules', () => {
expect(readonlyProp.isReadonly).toBeTruthy();
});
});

describe('getters and setters', () => {
let docs: DocCollection;
let foo1: PropertyMemberDoc;
let foo1Getter: AccessorInfoDoc;
let foo1Setter: AccessorInfoDoc;
let foo2: PropertyMemberDoc;
let foo2Getter: AccessorInfoDoc;
let foo2Setter: AccessorInfoDoc;
let bar: PropertyMemberDoc;
let barGetter: AccessorInfoDoc;
let barSetter: AccessorInfoDoc;
let qux: PropertyMemberDoc;
let quxGetter: AccessorInfoDoc;
let quxSetter: AccessorInfoDoc;
let noType: PropertyMemberDoc;
let noTypeGetter: AccessorInfoDoc;
let noTypeSetter: AccessorInfoDoc;

beforeEach(() => {
processor.sourceFiles = ['gettersAndSetters.ts'];
docs = [];
processor.$process(docs);

foo1 = docs.find(doc => doc.name === 'foo1');
foo2 = docs.find(doc => doc.name === 'foo2');
bar = docs.find(doc => doc.name === 'bar');
qux = docs.find(doc => doc.name === 'qux');
noType = docs.find(doc => doc.name === 'noType');

foo1Getter = docs.find(doc => doc.name === 'foo1:get');
foo1Setter = docs.find(doc => doc.name === 'foo1:set');

foo2Getter = docs.find(doc => doc.name === 'foo2:get');
foo2Setter = docs.find(doc => doc.name === 'foo2:set');

barGetter = docs.find(doc => doc.name === 'bar:get');
barSetter = docs.find(doc => doc.name === 'bar:set');

quxGetter = docs.find(doc => doc.name === 'qux:get');
quxSetter = docs.find(doc => doc.name === 'qux:set');

noTypeGetter = docs.find(doc => doc.name === 'noType:get');
noTypeSetter = docs.find(doc => doc.name === 'noType:set');
});

it('should create a property member doc for property that has accessors', () => {
expect(foo1).toBeDefined();
expect(foo1.docType).toBe('member');
expect(foo2).toBeDefined();
expect(foo2.docType).toBe('member');
expect(bar).toBeDefined();
expect(bar.docType).toBe('member');
expect(qux).toBeDefined();
expect(qux.docType).toBe('member');
});

it('should create a doc for each accessor of a property', () => {
expect(foo1Getter).toBeDefined();
expect(foo1Getter.docType).toBe('get-accessor-info');
expect(foo1Getter.content).toEqual('');
expect(foo1Setter).toBeDefined();
expect(foo1Setter.docType).toBe('set-accessor-info');
expect(foo1Setter.content).toEqual('foo1 setter');
expect(foo2Getter).toBeDefined();
expect(foo2Getter.docType).toBe('get-accessor-info');
expect(foo2Getter.content).toEqual('foo2 getter');
expect(foo2Setter).toBeDefined();
expect(foo2Setter.docType).toBe('set-accessor-info');
expect(foo2Setter.content).toEqual('');
expect(barGetter).toBeDefined();
expect(barGetter.docType).toBe('get-accessor-info');
expect(barGetter.content).toEqual('bar getter');
expect(barSetter).toBeUndefined();
expect(quxGetter).toBeUndefined();
expect(quxSetter).toBeDefined();
expect(quxSetter.docType).toBe('set-accessor-info');
expect(quxSetter.content).toEqual('qux setter');
});

it('should compute the type of the property from its accessors', () => {
expect(foo1.type).toEqual('string');
expect(foo2.type).toEqual('string');
expect(bar.type).toEqual('number');
expect(qux.type).toEqual('object');
expect(noType.type).toEqual('string');
});

it('should compute the content of the property from its accessors', () => {
expect(foo1.content).toEqual('foo1 setter');
expect(foo2.content).toEqual('foo2 getter');
expect(bar.content).toEqual('bar getter');
expect(qux.content).toEqual('qux setter');
expect(noType.content).toEqual('This has no explicit getter return type');
});

it('should attach each accessor doc to its property doc', () => {
expect(foo1.getAccessor).toBe(foo1Getter);
expect(foo1.setAccessor).toBe(foo1Setter);
expect(foo2.getAccessor).toBe(foo2Getter);
expect(foo2.setAccessor).toBe(foo2Setter);
expect(bar.getAccessor).toBe(barGetter);
expect(bar.setAccessor).toBe(null);
expect(qux.getAccessor).toBe(null);
expect(qux.setAccessor).toBe(quxSetter);
});
});
});

describe('strip namespaces', () => {
Expand Down
5 changes: 5 additions & 0 deletions typescript/src/processors/readTypeScriptModules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { InterfaceExportDoc } from '../../api-doc-types/InterfaceExportDoc';
import { MemberDoc } from '../../api-doc-types/MemberDoc';
import { MethodMemberDoc } from '../../api-doc-types/MethodMemberDoc';
import { ModuleDoc } from '../../api-doc-types/ModuleDoc';
import { PropertyMemberDoc } from '../../api-doc-types/PropertyMemberDoc';
import { TypeAliasExportDoc } from '../../api-doc-types/TypeAliasExportDoc';

import { expandSourceFiles, SourcePattern } from './SourcePattern';
Expand Down Expand Up @@ -151,6 +152,10 @@ export class ReadTypeScriptModules implements Processor {
members.forEach(member => {
docs.push(member);
if (member instanceof MethodMemberDoc) member.overloads.forEach(overloadDoc => docs.push(overloadDoc));
if (member instanceof PropertyMemberDoc) {
if (member.getAccessor) docs.push(member.getAccessor);
if (member.setAccessor) docs.push(member.setAccessor);
}
});
}
}
Expand Down