Skip to content

Commit fdb7964

Browse files
authored
Merge 2feb32b into cabc618
2 parents cabc618 + 2feb32b commit fdb7964

11 files changed

Lines changed: 295 additions & 7 deletions

File tree

.changeset/early-berries-live.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@lit-labs/analyzer': minor
3+
---
4+
5+
Adds support for overloaded functions. Methods of model objects that accept a
6+
string key will now specifically return the `FunctionDeclaration` of the
7+
implementation signature of an overloaded function, which has a new `overloads`
8+
field containing a `FunctionOverloadDeclaration` for each overload signature.

packages/labs/analyzer/src/lib/javascript/classes.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,16 @@ export const getClassMembers = (
8181
const methodMap = new Map<string, ClassMethod>();
8282
const staticMethodMap = new Map<string, ClassMethod>();
8383
declaration.members.forEach((node) => {
84-
if (ts.isMethodDeclaration(node)) {
84+
// Ignore non-implementation signatures of overloaded methods by checking
85+
// for `node.body`.
86+
if (ts.isMethodDeclaration(node) && node.body) {
8587
const info = getMemberInfo(node);
88+
const name = node.name.getText();
8689
(info.static ? staticMethodMap : methodMap).set(
87-
node.name.getText(),
90+
name,
8891
new ClassMethod({
8992
...info,
90-
...getFunctionLikeInfo(node, analyzer),
93+
...getFunctionLikeInfo(node, name, analyzer),
9194
...parseNodeJSDocInfo(node),
9295
})
9396
);

packages/labs/analyzer/src/lib/javascript/functions.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
AnalyzerInterface,
1717
DeclarationInfo,
1818
FunctionDeclaration,
19+
FunctionLikeInit,
20+
FunctionOverloadDeclaration,
1921
Parameter,
2022
Return,
2123
} from '../model.js';
@@ -68,9 +70,8 @@ export const getFunctionDeclaration = (
6870
docNode?: ts.Node
6971
): FunctionDeclaration => {
7072
return new FunctionDeclaration({
71-
name,
7273
...parseNodeJSDocInfo(docNode ?? declaration),
73-
...getFunctionLikeInfo(declaration, analyzer),
74+
...getFunctionLikeInfo(declaration, name, analyzer),
7475
});
7576
};
7677

@@ -79,11 +80,39 @@ export const getFunctionDeclaration = (
7980
*/
8081
export const getFunctionLikeInfo = (
8182
node: ts.FunctionLikeDeclaration,
83+
name: string,
8284
analyzer: AnalyzerInterface
83-
) => {
85+
): FunctionLikeInit => {
86+
let overloads = undefined;
87+
if (node.body) {
88+
// Overloaded functions have multiple declaration nodes.
89+
const type = analyzer.program.getTypeChecker().getTypeAtLocation(node);
90+
const overloadDeclarations = type
91+
.getSymbol()
92+
?.getDeclarations()
93+
?.filter((x) => x !== node) as Array<ts.FunctionLikeDeclaration>;
94+
95+
overloads = overloadDeclarations?.map((overload) => {
96+
const info = getFunctionLikeInfo(overload, name, analyzer);
97+
return new FunctionOverloadDeclaration({
98+
// `docNode ?? overload` isn't needed here because TS doesn't allow
99+
// const function assignments to be overloaded as of now.
100+
...parseNodeJSDocInfo(overload),
101+
102+
// `info` can't be spread because `FunctionLikeInit` has an `overloads`
103+
// property, even though it's always `undefined` in this case.
104+
name: info.name,
105+
parameters: info.parameters,
106+
return: info.return,
107+
});
108+
});
109+
}
110+
84111
return {
112+
name,
85113
parameters: node.parameters.map((p) => getParameter(p, analyzer)),
86114
return: getReturn(node, analyzer),
115+
overloads,
87116
};
88117
};
89118

packages/labs/analyzer/src/lib/javascript/modules.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ export const getModule = (
117117
for (const statement of sourceFile.statements) {
118118
if (ts.isClassDeclaration(statement)) {
119119
addDeclaration(getClassDeclarationInfo(statement, analyzer));
120-
} else if (ts.isFunctionDeclaration(statement)) {
120+
// Ignore non-implementation signatures of overloaded functions by
121+
// checking for `statement.body`.
122+
} else if (ts.isFunctionDeclaration(statement) && statement.body) {
121123
addDeclaration(getFunctionDeclarationInfo(statement, analyzer));
122124
} else if (ts.isVariableStatement(statement)) {
123125
getVariableDeclarationInfo(statement, analyzer).forEach(addDeclaration);

packages/labs/analyzer/src/lib/model.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,15 +330,29 @@ export interface FunctionLikeInit extends DeprecatableDescribed {
330330
name: string;
331331
parameters?: Parameter[] | undefined;
332332
return?: Return | undefined;
333+
overloads?: FunctionOverloadDeclaration[] | undefined;
333334
}
334335

335336
export class FunctionDeclaration extends Declaration {
336337
parameters?: Parameter[] | undefined;
337338
return?: Return | undefined;
339+
overloads?: FunctionOverloadDeclaration[] | undefined;
338340
constructor(init: FunctionLikeInit) {
339341
super(init);
340342
this.parameters = init.parameters;
341343
this.return = init.return;
344+
this.overloads = init.overloads;
345+
}
346+
}
347+
348+
export interface FunctionLikeOverloadInit extends FunctionLikeInit {
349+
overloads?: undefined;
350+
}
351+
352+
export class FunctionOverloadDeclaration extends FunctionDeclaration {
353+
override overloads: undefined;
354+
constructor(init: FunctionLikeOverloadInit) {
355+
super(init);
342356
}
343357
}
344358

packages/labs/analyzer/src/test/javascript/functions_test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,5 +179,84 @@ for (const lang of languages) {
179179
assert.equal(fn.deprecated, 'Async function deprecated');
180180
});
181181

182+
test('Overloaded function', ({module}) => {
183+
const exportedFn = module.getResolvedExport('overloaded');
184+
const fn = module.getDeclaration('overloaded');
185+
assert.equal(fn, exportedFn);
186+
assert.ok(fn?.isFunctionDeclaration());
187+
assert.equal(fn.name, 'overloaded');
188+
assert.equal(
189+
fn.description,
190+
'This signature works with strings or numbers.'
191+
);
192+
assert.equal(fn.summary, undefined);
193+
assert.equal(fn.parameters?.length, 1);
194+
assert.equal(fn.parameters?.[0].name, 'x');
195+
assert.equal(
196+
fn.parameters?.[0].description,
197+
'Accepts either a string or a number.'
198+
);
199+
assert.equal(fn.parameters?.[0].summary, undefined);
200+
assert.equal(fn.parameters?.[0].type?.text, 'string | number');
201+
assert.equal(fn.parameters?.[0].default, undefined);
202+
assert.equal(fn.parameters?.[0].rest, false);
203+
assert.equal(fn.return?.type?.text, 'string | number');
204+
assert.equal(
205+
fn.return?.description,
206+
'Returns either a string or a number.'
207+
);
208+
assert.equal(fn.deprecated, undefined);
209+
210+
// TODO: Run the same assertions in both languages once TS supports
211+
// `@overload` for JSDoc in JS.
212+
// <https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-rc/#overload-support-in-jsdoc>
213+
if (lang === 'ts') {
214+
assert.ok(fn.overloads);
215+
assert.equal(fn.overloads.length, 2);
216+
217+
assert.equal(fn.overloads[0].name, 'overloaded');
218+
assert.equal(
219+
fn.overloads[0].description,
220+
'This signature only works with strings.'
221+
);
222+
assert.equal(fn.overloads[0].summary, undefined);
223+
assert.equal(fn.overloads[0].parameters?.length, 1);
224+
assert.equal(fn.overloads[0].parameters?.[0].name, 'x');
225+
assert.equal(
226+
fn.overloads[0].parameters?.[0].description,
227+
'Accepts a string.'
228+
);
229+
assert.equal(fn.overloads[0].parameters?.[0].summary, undefined);
230+
assert.equal(fn.overloads[0].parameters?.[0].type?.text, 'string');
231+
assert.equal(fn.overloads[0].parameters?.[0].default, undefined);
232+
assert.equal(fn.overloads[0].parameters?.[0].rest, false);
233+
assert.equal(fn.overloads[0].return?.type?.text, 'string');
234+
assert.equal(fn.overloads[0].return?.description, 'Returns a string.');
235+
assert.equal(fn.overloads[0].deprecated, undefined);
236+
237+
assert.equal(fn.overloads[1].name, 'overloaded');
238+
assert.equal(
239+
fn.overloads[1].description,
240+
'This signature only works with numbers.'
241+
);
242+
assert.equal(fn.overloads[1].summary, undefined);
243+
assert.equal(fn.overloads[1].parameters?.length, 1);
244+
assert.equal(fn.overloads[1].parameters?.[0].name, 'x');
245+
assert.equal(
246+
fn.overloads[1].parameters?.[0].description,
247+
'Accepts a number.'
248+
);
249+
assert.equal(fn.overloads[1].parameters?.[0].summary, undefined);
250+
assert.equal(fn.overloads[1].parameters?.[0].type?.text, 'number');
251+
assert.equal(fn.overloads[1].parameters?.[0].default, undefined);
252+
assert.equal(fn.overloads[1].parameters?.[0].rest, false);
253+
assert.equal(fn.overloads[1].return?.type?.text, 'number');
254+
assert.equal(fn.overloads[1].return?.description, 'Returns a number.');
255+
assert.equal(fn.overloads[1].deprecated, undefined);
256+
} else {
257+
assert.equal(fn.overloads?.length ?? 0, 0);
258+
}
259+
});
260+
182261
test.run();
183262
}

packages/labs/analyzer/src/test/lit-element/properties_test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,5 +191,82 @@ for (const lang of languages) {
191191
assert.equal(property.attribute, 'static-prop');
192192
});
193193

194+
test('method with an overloaded signature', ({element}) => {
195+
const fn = Array.from(element.methods).find((m) => m.name === 'overloaded');
196+
assert.ok(fn?.isFunctionDeclaration());
197+
assert.equal(fn.name, 'overloaded');
198+
assert.equal(
199+
fn.description,
200+
'This signature works with strings or numbers.'
201+
);
202+
assert.equal(fn.summary, undefined);
203+
assert.equal(fn.parameters?.length, 1);
204+
assert.equal(fn.parameters?.[0].name, 'x');
205+
assert.equal(
206+
fn.parameters?.[0].description,
207+
'Accepts either a string or a number.'
208+
);
209+
assert.equal(fn.parameters?.[0].summary, undefined);
210+
assert.equal(fn.parameters?.[0].type?.text, 'string | number');
211+
assert.equal(fn.parameters?.[0].default, undefined);
212+
assert.equal(fn.parameters?.[0].rest, false);
213+
assert.equal(fn.return?.type?.text, 'string | number');
214+
assert.equal(
215+
fn.return?.description,
216+
'Returns either a string or a number.'
217+
);
218+
assert.equal(fn.deprecated, undefined);
219+
220+
// TODO: Run the same assertions in both languages once TS supports
221+
// `@overload` for JSDoc in JS.
222+
// <https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-rc/#overload-support-in-jsdoc>
223+
if (lang === 'ts') {
224+
assert.ok(fn.overloads);
225+
assert.equal(fn.overloads.length, 2);
226+
227+
assert.equal(fn.overloads[0].name, 'overloaded');
228+
assert.equal(
229+
fn.overloads[0].description,
230+
'This signature only works with strings.'
231+
);
232+
assert.equal(fn.overloads[0].summary, undefined);
233+
assert.equal(fn.overloads[0].parameters?.length, 1);
234+
assert.equal(fn.overloads[0].parameters?.[0].name, 'x');
235+
assert.equal(
236+
fn.overloads[0].parameters?.[0].description,
237+
'Accepts a string.'
238+
);
239+
assert.equal(fn.overloads[0].parameters?.[0].summary, undefined);
240+
assert.equal(fn.overloads[0].parameters?.[0].type?.text, 'string');
241+
assert.equal(fn.overloads[0].parameters?.[0].default, undefined);
242+
assert.equal(fn.overloads[0].parameters?.[0].rest, false);
243+
assert.equal(fn.overloads[0].return?.type?.text, 'string');
244+
assert.equal(fn.overloads[0].return?.description, 'Returns a string.');
245+
assert.equal(fn.overloads[0].deprecated, undefined);
246+
247+
assert.equal(fn.overloads[1].name, 'overloaded');
248+
assert.equal(
249+
fn.overloads[1].description,
250+
'This signature only works with numbers.'
251+
);
252+
assert.equal(fn.overloads[1].summary, undefined);
253+
assert.equal(fn.overloads[1].parameters?.length, 1);
254+
assert.equal(fn.overloads[1].parameters?.[0].name, 'x');
255+
assert.equal(
256+
fn.overloads[1].parameters?.[0].description,
257+
'Accepts a number.'
258+
);
259+
assert.equal(fn.overloads[1].parameters?.[0].summary, undefined);
260+
assert.equal(fn.overloads[1].parameters?.[0].type?.text, 'number');
261+
assert.equal(fn.overloads[1].parameters?.[0].default, undefined);
262+
assert.equal(fn.overloads[1].parameters?.[0].rest, false);
263+
assert.equal(fn.overloads[1].return?.type?.text, 'number');
264+
assert.equal(fn.overloads[1].return?.description, 'Returns a number.');
265+
assert.equal(fn.overloads[1].deprecated, undefined);
266+
} else {
267+
assert.equal(fn.overloads?.length ?? 0, 0);
268+
}
269+
});
270+
194271
test.run();
195272
}

packages/labs/analyzer/test-files/js/functions/functions.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,16 @@ export const asyncFunction = async (a) => {
8787
await 0;
8888
return a;
8989
};
90+
91+
/**
92+
* This signature works with strings or numbers.
93+
* @param {string | number} x Accepts either a string or a number.
94+
* @returns {string | number} Returns either a string or a number.
95+
*/
96+
export function overloaded(x) {
97+
if (typeof x === 'string') {
98+
return x + 'abc';
99+
} else {
100+
return x + 123;
101+
}
102+
}

packages/labs/analyzer/test-files/js/properties/element-a.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,18 @@ export class ElementA extends LitElement {
5656
this.globalClass = document.createElement('foo');
5757
this.staticProp = 42;
5858
}
59+
60+
/**
61+
* This signature works with strings or numbers.
62+
* @param {string | number} x Accepts either a string or a number.
63+
* @returns {string | number} Returns either a string or a number.
64+
*/
65+
overloaded(x) {
66+
if (typeof x === 'string') {
67+
return x + 'abc';
68+
} else {
69+
return x + 123;
70+
}
71+
}
5972
}
6073
customElements.define('element-a', ElementA);

packages/labs/analyzer/test-files/ts/functions/src/functions.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,28 @@ export const asyncFunction = async (a: string) => {
9191
await 0;
9292
return a;
9393
};
94+
95+
/**
96+
* This signature only works with strings.
97+
* @param x Accepts a string.
98+
* @returns Returns a string.
99+
*/
100+
export function overloaded(x: string): string;
101+
/**
102+
* This signature only works with numbers.
103+
* @param x Accepts a number.
104+
* @returns Returns a number.
105+
*/
106+
export function overloaded(x: number): number;
107+
/**
108+
* This signature works with strings or numbers.
109+
* @param x Accepts either a string or a number.
110+
* @returns Returns either a string or a number.
111+
*/
112+
export function overloaded(x: string | number): string | number {
113+
if (typeof x === 'string') {
114+
return x + 'abc';
115+
} else {
116+
return x + 123;
117+
}
118+
}

0 commit comments

Comments
 (0)