Skip to content

Commit ad0910b

Browse files
committed
Add support event types in JSDoc. Fixes #165
1 parent 432d31f commit ad0910b

File tree

21 files changed

+303
-95
lines changed

21 files changed

+303
-95
lines changed

dev/src/custom-element/custom-element.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export class CustomElement extends MySuperClass {
2828
set attr1(val: string) {}
2929

3030
onClick() {
31-
new CustomEvent("my-custom-event");
31+
this.dispatchEvent(new CustomEvent("my-custom-event", { detail: "hello" }));
32+
this.dispatchEvent(new MouseEvent("mouse-move"));
3233
}
3334
}
3435

dev/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"experimentalDecorators": true,
44
"target": "es5",
55
"module": "commonjs",
6-
"lib": ["esnext", "dom"],
6+
"lib": ["ESnext", "DOM"],
77
"strict": true,
88
"esModuleInterop": true
99
}

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"homepage": "https://github.com/runem/web-component-analyzer#readme",
4848
"dependencies": {
4949
"fast-glob": "^3.2.2",
50-
"ts-simple-type": "~1.0.0",
50+
"ts-simple-type": "~1.0.4",
5151
"typescript": "^3.8.3",
5252
"yargs": "^15.3.1"
5353
},
@@ -87,7 +87,7 @@
8787
"test/**/*.ts",
8888
"!test/{helpers,snapshots}/**/*"
8989
],
90-
"timeout": "200s"
90+
"timeout": "2m"
9191
},
9292
"husky": {
9393
"hooks": {

src/analyze/analyze-text.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { existsSync, readFileSync } from "fs";
2+
import { dirname, join } from "path";
13
import * as tsModule from "typescript";
24
import { CompilerOptions, Program, ScriptKind, ScriptTarget, SourceFile, System, TypeChecker } from "typescript";
35
//import * as ts from "typescript";
@@ -10,6 +12,7 @@ export interface IVirtualSourceFile {
1012
fileName: string;
1113
text?: string;
1214
analyze?: boolean;
15+
includeLib?: boolean;
1316
}
1417

1518
export type VirtualSourceFile = IVirtualSourceFile | string;
@@ -45,9 +48,24 @@ export function analyzeText(inputFiles: VirtualSourceFile[] | VirtualSourceFile,
4548
)
4649
.map(file => ({ ...file, fileName: file.fileName }));
4750

51+
const includeLib = files.some(file => file.includeLib);
52+
4853
const readFile = (fileName: string): string | undefined => {
4954
const matchedFile = files.find(currentFile => currentFile.fileName === fileName);
50-
return matchedFile == null ? undefined : matchedFile.text;
55+
if (matchedFile != null) {
56+
return matchedFile.text;
57+
}
58+
59+
if (includeLib) {
60+
// TODO: find better method of finding the current typescript module path
61+
fileName = fileName.match(/[/\\]/) ? fileName : join(dirname(require.resolve("typescript")), fileName);
62+
}
63+
64+
if (existsSync(fileName)) {
65+
return readFileSync(fileName, "utf8").toString();
66+
}
67+
68+
return undefined;
5169
};
5270

5371
const fileExists = (fileName: string): boolean => {

src/analyze/analyzer-visit-context.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as tsModule from "typescript";
2-
import { Node, SourceFile, TypeChecker } from "typescript";
2+
import { Node, SourceFile, TypeChecker, Program } from "typescript";
33
import { AnalyzerFlavor, ComponentFeatureCollection } from "./flavors/analyzer-flavor";
44
import { AnalyzerConfig } from "./types/analyzer-config";
55
import { ComponentDeclaration } from "./types/component-declaration";
@@ -10,6 +10,7 @@ import { ComponentDeclaration } from "./types/component-declaration";
1010
*/
1111
export interface AnalyzerVisitContext {
1212
checker: TypeChecker;
13+
program: Program;
1314
ts: typeof tsModule;
1415
config: AnalyzerConfig;
1516
flavors: AnalyzerFlavor[];

src/analyze/flavors/custom-element/discover-events.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
1-
import { SimpleType } from "ts-simple-type";
21
import { Node } from "typescript";
32
import { AnalyzerVisitContext } from "../../analyzer-visit-context";
43
import { ComponentEvent } from "../../types/features/component-event";
54
import { getJsDoc } from "../../util/js-doc-util";
65
import { lazy } from "../../util/lazy";
6+
import { resolveNodeValue } from "../../util/resolve-node-value";
7+
8+
const EVENT_NAMES = [
9+
"Event",
10+
"CustomEvent",
11+
"AnimationEvent",
12+
"ClipboardEvent",
13+
"DragEvent",
14+
"FocusEvent",
15+
"HashChangeEvent",
16+
"InputEvent",
17+
"KeyboardEvent",
18+
"MouseEvent",
19+
"PageTransitionEvent",
20+
"PopStateEvent",
21+
"ProgressEvent",
22+
"StorageEvent",
23+
"TouchEvent",
24+
"TransitionEvent",
25+
"UiEvent",
26+
"WheelEvent"
27+
];
728

829
/**
930
* Discovers events dispatched
@@ -15,14 +36,14 @@ export function discoverEvents(node: Node, context: AnalyzerVisitContext): Compo
1536

1637
// new CustomEvent("my-event");
1738
if (ts.isNewExpression(node)) {
18-
const { expression, arguments: args, typeArguments } = node;
39+
const { expression, arguments: args } = node;
1940

20-
if (expression.getText() === "CustomEvent" && args && args.length >= 1) {
41+
if (EVENT_NAMES.includes(expression.getText()) && args && args.length >= 1) {
2142
const arg = args[0];
2243

23-
if (ts.isStringLiteralLike(arg)) {
24-
const eventName = arg.text;
44+
const eventName = resolveNodeValue(arg, context)?.value;
2545

46+
if (typeof eventName === "string") {
2647
// Either grab jsdoc from the new expression or from a possible call expression that its wrapped in
2748
const jsDoc =
2849
getJsDoc(expression, ts) ||
@@ -35,14 +56,7 @@ export function discoverEvents(node: Node, context: AnalyzerVisitContext): Compo
3556
jsDoc,
3657
name: eventName,
3758
node,
38-
type: lazy(() => {
39-
return (
40-
(typeArguments?.[0] != null && checker.getTypeFromTypeNode(typeArguments[0])) ||
41-
({
42-
kind: "ANY"
43-
} as SimpleType)
44-
);
45-
})
59+
type: lazy(() => checker.getTypeAtLocation(node))
4660
}
4761
];
4862
}

src/analyze/flavors/js-doc/discover-features.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const discoverFeatures: Partial<FeatureDiscoverVisitMap<AnalyzerVisitCont
5959
return {
6060
name: name,
6161
jsDoc: description != null ? { description } : undefined,
62-
type: lazy(() => (type && parseSimpleJsDocTypeExpression(type)) || { kind: "ANY" }),
62+
type: type != null ? lazy(() => parseSimpleJsDocTypeExpression(type, context) || { kind: "ANY" }) : undefined,
6363
typeHint: type,
6464
node: tagNode
6565
};
@@ -77,7 +77,7 @@ export const discoverFeatures: Partial<FeatureDiscoverVisitMap<AnalyzerVisitCont
7777
(tagNode, { name, type, description }) => {
7878
// Grab the type from jsdoc and use it to find permitted tag names
7979
// Example: @slot {"div"|"span"} myslot
80-
const permittedTagNameType = type == null ? undefined : parseSimpleJsDocTypeExpression(type);
80+
const permittedTagNameType = type == null ? undefined : parseSimpleJsDocTypeExpression(type, context);
8181
const permittedTagNames: string[] | undefined = (() => {
8282
if (permittedTagNameType == null) {
8383
return undefined;
@@ -120,7 +120,7 @@ export const discoverFeatures: Partial<FeatureDiscoverVisitMap<AnalyzerVisitCont
120120
propName: name,
121121
jsDoc: description != null ? { description } : undefined,
122122
typeHint: type,
123-
type: lazy(() => (type && parseSimpleJsDocTypeExpression(type)) || { kind: "ANY" }),
123+
type: lazy(() => (type && parseSimpleJsDocTypeExpression(type, context)) || { kind: "ANY" }),
124124
node: tagNode,
125125
default: def,
126126
visibility: undefined,
@@ -143,7 +143,7 @@ export const discoverFeatures: Partial<FeatureDiscoverVisitMap<AnalyzerVisitCont
143143
kind: "attribute",
144144
attrName: name,
145145
jsDoc: description != null ? { description } : undefined,
146-
type: lazy(() => (type && parseSimpleJsDocTypeExpression(type)) || { kind: "ANY" }),
146+
type: lazy(() => (type && parseSimpleJsDocTypeExpression(type, context)) || { kind: "ANY" }),
147147
typeHint: type,
148148
node: tagNode,
149149
default: def,

src/analyze/flavors/js-doc/refine-feature.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { ComponentEvent } from "../../types/features/component-event";
1+
import { AnalyzerVisitContext } from "../../analyzer-visit-context";
22
import { ComponentMember, ComponentMemberReflectKind } from "../../types/features/component-member";
3-
import { ComponentMethod } from "../../types/features/component-method";
43
import { JsDoc } from "../../types/js-doc";
54
import { VisibilityKind } from "../../types/visibility-kind";
65
import { parseSimpleJsDocTypeExpression } from "../../util/js-doc-util";
@@ -11,7 +10,7 @@ import { AnalyzerFlavor } from "../analyzer-flavor";
1110
* Refines features by looking at the jsdoc tags on the feature
1211
*/
1312
export const refineFeature: AnalyzerFlavor["refineFeature"] = {
14-
event: (event: ComponentEvent) => {
13+
event: (event, context) => {
1514
if (event.jsDoc == null || event.jsDoc.tags == null) return event;
1615

1716
// Check if the feature has "@ignore" jsdoc tag
@@ -20,23 +19,26 @@ export const refineFeature: AnalyzerFlavor["refineFeature"] = {
2019
}
2120

2221
return [applyJsDocDeprecated, applyJsDocVisibility, applyJsDocType].reduce(
23-
(event, applyFunc) => (applyFunc as Function)(event, event.jsDoc),
22+
(event, applyFunc) => (applyFunc as Function)(event, event.jsDoc, context),
2423
event
2524
);
2625
},
27-
method: (method: ComponentMethod) => {
26+
method: (method, context) => {
2827
if (method.jsDoc == null || method.jsDoc.tags == null) return method;
2928

3029
// Check if the feature has "@ignore" jsdoc tag
3130
if (hasIgnoreJsDocTag(method.jsDoc)) {
3231
return undefined;
3332
}
3433

35-
method = [applyJsDocDeprecated, applyJsDocVisibility].reduce((method, applyFunc) => (applyFunc as Function)(method, method.jsDoc), method);
34+
method = [applyJsDocDeprecated, applyJsDocVisibility].reduce(
35+
(method, applyFunc) => (applyFunc as Function)(method, method.jsDoc, context),
36+
method
37+
);
3638

3739
return method;
3840
},
39-
member: (member: ComponentMember) => {
41+
member: (member, context) => {
4042
// Return right away if the member doesn't have jsdoc
4143
if (member.jsDoc == null || member.jsDoc.tags == null) return member;
4244

@@ -54,7 +56,7 @@ export const refineFeature: AnalyzerFlavor["refineFeature"] = {
5456
applyJsDocType,
5557
applyJsDocAttribute,
5658
applyJsDocModifiers
57-
].reduce((member, applyFunc) => (applyFunc as Function)(member, member.jsDoc), member);
59+
].reduce((member, applyFunc) => (applyFunc as Function)(member, member.jsDoc, context), member);
5860
}
5961
};
6062

@@ -122,10 +124,12 @@ function applyJsDocVisibility<T extends Partial<Pick<ComponentMember, "visibilit
122124
* Applies the "@attribute" jsdoc tag
123125
* @param feature
124126
* @param jsDoc
127+
* @param context
125128
*/
126129
function applyJsDocAttribute<T extends Partial<Pick<ComponentMember, "propName" | "attrName" | "default" | "type" | "typeHint">>>(
127130
feature: T,
128-
jsDoc: JsDoc
131+
jsDoc: JsDoc,
132+
context: AnalyzerVisitContext
129133
): T {
130134
const attributeTag = jsDoc.tags?.find(tag => ["attr", "attribute"].includes(tag.tag));
131135

@@ -141,7 +145,7 @@ function applyJsDocAttribute<T extends Partial<Pick<ComponentMember, "propName"
141145
// @attr jsdoc tag can also include the type of attribute
142146
if (parsed.type != null && result.typeHint == null) {
143147
result.typeHint = parsed.type;
144-
result.type = feature.type ?? lazy(() => parseSimpleJsDocTypeExpression(parsed.type || ""));
148+
result.type = feature.type ?? lazy(() => parseSimpleJsDocTypeExpression(parsed.type || "", context));
145149
}
146150

147151
return result;
@@ -237,8 +241,9 @@ function applyJsDocReflect<T extends Partial<Pick<ComponentMember, "reflect">>>(
237241
* Applies the "@type" jsdoc tag
238242
* @param feature
239243
* @param jsDoc
244+
* @param context
240245
*/
241-
function applyJsDocType<T extends Partial<Pick<ComponentMember, "type" | "typeHint">>>(feature: T, jsDoc: JsDoc): T {
246+
function applyJsDocType<T extends Partial<Pick<ComponentMember, "type" | "typeHint">>>(feature: T, jsDoc: JsDoc, context: AnalyzerVisitContext): T {
242247
const typeTag = jsDoc.tags?.find(tag => tag.tag === "type");
243248

244249
if (typeTag != null && feature.typeHint == null) {
@@ -248,7 +253,7 @@ function applyJsDocType<T extends Partial<Pick<ComponentMember, "type" | "typeHi
248253
return {
249254
...feature,
250255
typeHint: parsed.type,
251-
type: feature.type ?? lazy(() => parseSimpleJsDocTypeExpression(parsed.type || ""))
256+
type: feature.type ?? lazy(() => parseSimpleJsDocTypeExpression(parsed.type || "", context))
252257
};
253258
}
254259
}

src/analyze/flavors/lit-element/discover-members.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ function parseStaticProperties(returnStatement: ReturnStatement, context: Analyz
202202
priority: "high",
203203
kind: "property",
204204
type: lazy(() => {
205-
return (jsDoc && getJsDocType(jsDoc)) || (typeof litConfig.type === "object" && litConfig.type) || { kind: "ANY" };
205+
return (jsDoc && getJsDocType(jsDoc, context)) || (typeof litConfig.type === "object" && litConfig.type) || { kind: "ANY" };
206206
}),
207207
propName: propName,
208208
attrName: emitAttribute ? attrName : undefined,

0 commit comments

Comments
 (0)