fix: correctly track interface members

When an interface is a) defined as an ES2015 class and b) assigned to a variable, JSDoc sometimes used the wrong `longname` and `memberof` for members of the interface (specifically, for instance properties).

The root cause was that we weren't resolving `this` correctly within this type of interface. As a result, if you added a JSDoc comment to something like `this.foo = 'bar'`, the doclet for `this.foo` had the wrong `longname` and `memberof`.

Fixing that issue uncovered another issue: When we merged the constructor's doclet with the interface's doclet, we preferred the constructor's doclet. However, the constructor's doclet used the wrong `kind` in this case; we already had code to fix up the `longname` and `memberof` of the combined doclet, but not the `kind`. The fix was to prefer the interface's doclet for all properties.

Manual cherry-pick of 95e3192525310b9f1567e034c22489da3a5847a1.
This commit is contained in:
Jeff Williams 2020-12-12 19:24:20 -08:00
parent c9b12b09ec
commit eaa2cfb807
5 changed files with 86 additions and 5 deletions

View File

@ -531,7 +531,8 @@ class Parser extends EventEmitter {
result = doclet.longname; result = doclet.longname;
} }
// walk up to the closest class we can find // walk up to the closest class we can find
else if (doclet.kind === 'class' || doclet.kind === 'module') { else if (doclet.kind === 'class' || doclet.kind === 'interface' ||
doclet.kind === 'module') {
result = doclet.longname; result = doclet.longname;
} }
else if (node.enclosingScope) { else if (node.enclosingScope) {

View File

@ -319,11 +319,9 @@ function makeConstructorFinisher(parser) {
return; return;
} }
// We prefer the parent doclet because it has the correct kind, longname, and memberof.
// The child doclet might or might not have the correct kind, longname, and memberof.
combined = combineDoclets(parentDoclet, eventDoclet); combined = combineDoclets(parentDoclet, eventDoclet);
combined.longname = parentDoclet.longname;
if (parentDoclet.memberof) {
combined.memberof = parentDoclet.memberof;
}
parser.addResult(combined); parser.addResult(combined);

View File

@ -0,0 +1,15 @@
/** @namespace */
const myCorp = {};
/**
* @interface
*/
myCorp.IWorker = class IWorker {
constructor(workerName) {
/** Name of the worker. */
this.workerName = workerName;
}
/** Interface for doing some work. */
work() {}
};

View File

@ -0,0 +1,23 @@
/**
* @interface
*/
class IWorker {
/** Interface for doing some work. */
work() {}
}
/**
* @implements {IWorker}
*/
class MyWorker {
/** Do some work. */
work() {}
/** Process a thing. */
process() {}
}
/**
* @implements {IWorker}
*/
class MyIncompleteWorker {}

View File

@ -54,6 +54,50 @@ describe('@interface tag', () => {
}); });
}); });
describe('ES2015 classes as interfaces', () => {
const docSet2 = jsdoc.getDocSetFromFile('test/fixtures/interface-implements2.js');
const docSet3 = jsdoc.getDocSetFromFile('test/fixtures/interface-assignment.js');
it('should set the correct kind on the interface', () => {
const workerInterface = docSet2.getByLongname('IWorker').filter(d => !d.undocumented)[0];
expect(workerInterface.kind).toBe('interface');
});
it('should set the correct kind on methods in the interface', () => {
const workerInterfaceWork = docSet2.getByLongname('IWorker#work')
.filter(d => !d.undocumented)[0];
expect(workerInterfaceWork.kind).toBe('function');
});
it('should set the correct kind on the implementing class', () => {
const workerImpl = docSet2.getByLongname('MyWorker').filter(d => !d.undocumented)[0];
expect(workerImpl.kind).toBe('class');
});
it('should set the correct kind on an interface assigned to a variable', () => {
const workerInterface = docSet3.getByLongname('myCorp.IWorker').filter(d => !d.undocumented)[0];
expect(workerInterface.kind).toBe('interface');
});
it('should set the correct kind on methods in an interface assigned to a variable', () => {
const workerInterfaceWork = docSet3.getByLongname('myCorp.IWorker#work')
.filter(d => !d.undocumented)[0];
expect(workerInterfaceWork.kind).toBe('function');
});
it('should set the correct kind on other members in an interface assigned to a variable', () => {
const workerName = docSet3.getByLongname('myCorp.IWorker#workerName')
.filter(d => !d.undocumented)[0];
expect(workerName.kind).toBe('member');
});
});
describe('Closure Compiler tags', () => { describe('Closure Compiler tags', () => {
afterEach(() => { afterEach(() => {
jsdoc.restoreTagDictionary(); jsdoc.restoreTagDictionary();