Skip to content

Commit

Permalink
Add support for JSDoc syntax where type comes after name. #165
Browse files Browse the repository at this point in the history
  • Loading branch information
runem committed Jul 12, 2020
1 parent 4e57b34 commit bd0cff3
Show file tree
Hide file tree
Showing 20 changed files with 127 additions and 108 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/)

### Fixed

- WCA now resolves more declarations and mixins patterns ([#172](https://github.com/runem/web-component-analyzer/issues/172))
- Improved logic for resolving declarations and mixins ([#172](https://github.com/runem/web-component-analyzer/issues/172))
- Added support for JSDoc syntax where type comes after name (eg. `@fires my-event {MouseEvent}`) ([#165](https://github.com/runem/web-component-analyzer/issues/165))

### Changes

Expand Down
167 changes: 93 additions & 74 deletions src/analyze/util/js-doc-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ function parseJsDocValue(value: string | undefined): unknown {
}

/**
* Parses "@tag {type} name description"
* Parses "@tag {type} name description" or "@tag name {type} description"
* @param str
*/
function parseJsDocTagString(str: string): JsDocTagParsed {
Expand All @@ -289,87 +289,106 @@ function parseJsDocTagString(str: string): JsDocTagParsed {
return quotedStr.replace(/^['"](.+)["']$/, (_, match) => match);
};

// Match tag
// Example: " @mytag"
const tagResult = str.match(/^(\s*@(\S+))/);
if (tagResult == null) {
return jsDocTag;
} else {
// Move string to the end of the match
// Example: " @mytag|"
moveStr(tagResult[1]);
jsDocTag.tag = tagResult[2];
}

// Match type
// Example: " {MyType}"
const typeResult = str.match(/^(\s*{([\s\S]*)})/);
if (typeResult != null) {
// Move string to the end of the match
// Example: " {MyType}|"
moveStr(typeResult[1]);
jsDocTag.type = typeResult[2];
}
const matchTag = () => {
// Match tag
// Example: " @mytag"
const tagResult = str.match(/^(\s*@(\S+))/);
if (tagResult == null) {
return jsDocTag;
} else {
// Move string to the end of the match
// Example: " @mytag|"
moveStr(tagResult[1]);
jsDocTag.tag = tagResult[2];
}
};

// Match optional name
// Example: " [myname=mydefault]"
const defaultNameResult = str.match(/^(\s*\[([\s\S]+)\])/);
if (defaultNameResult != null) {
// Move string to the end of the match
// Example: " [myname=mydefault]|"
moveStr(defaultNameResult[1]);

// Using [...] means that this doc is optional
jsDocTag.optional = true;

// Split the inner content between [...] into parts
// Example: "myname=mydefault" => "myname", "mydefault"
const parts = defaultNameResult[2].split("=");
if (parts.length === 2) {
// Both name and default were given
jsDocTag.name = unqouteStr(parts[0]);
jsDocTag.default = parseJsDocValue(parts[1]);
} else if (parts.length !== 0) {
// No default was given
jsDocTag.name = unqouteStr(parts[0]);
const matchType = () => {
// Match type
// Example: " {MyType}"
const typeResult = str.match(/^(\s*{([\s\S]*)})/);
if (typeResult != null) {
// Move string to the end of the match
// Example: " {MyType}|"
moveStr(typeResult[1]);
jsDocTag.type = typeResult[2];
}
} else {
// else, match required name
// Example: " myname"

// A name is needed some jsdoc tags making it possible to include omit "-"
// Therefore we don't look for "-" or line end if the name is required - in that case we only need to eat the first word to find the name.
const regex = JSDOC_TAGS_WITH_REQUIRED_NAME.includes(jsDocTag.tag) ? /^(\s*(\S+))/ : /^(\s*(\S+))((\s*-[\s\S]+)|\s*)($|[\r\n])/;
const nameResult = str.match(regex);
if (nameResult != null) {
// Move string to end of match
// Example: " myname|"
moveStr(nameResult[1]);
jsDocTag.name = unqouteStr(nameResult[2].trim());
};

const matchName = () => {
// Match optional name
// Example: " [myname=mydefault]"
const defaultNameResult = str.match(/^(\s*\[([\s\S]+)\])/);
if (defaultNameResult != null) {
// Move string to the end of the match
// Example: " [myname=mydefault]|"
moveStr(defaultNameResult[1]);

// Using [...] means that this doc is optional
jsDocTag.optional = true;

// Split the inner content between [...] into parts
// Example: "myname=mydefault" => "myname", "mydefault"
const parts = defaultNameResult[2].split("=");
if (parts.length === 2) {
// Both name and default were given
jsDocTag.name = unqouteStr(parts[0]);
jsDocTag.default = parseJsDocValue(parts[1]);
} else if (parts.length !== 0) {
// No default was given
jsDocTag.name = unqouteStr(parts[0]);
}
} else {
// else, match required name
// Example: " myname"

// A name is needed some jsdoc tags making it possible to include omit "-"
// Therefore we don't look for "-" or line end if the name is required - in that case we only need to eat the first word to find the name.
const regex = JSDOC_TAGS_WITH_REQUIRED_NAME.includes(jsDocTag.tag) ? /^(\s*(\S+))/ : /^(\s*(\S+))((\s*-[\s\S]+)|\s*)($|[\r\n])/;
const nameResult = str.match(regex);
if (nameResult != null) {
// Move string to end of match
// Example: " myname|"
moveStr(nameResult[1]);
jsDocTag.name = unqouteStr(nameResult[2].trim());
}
}
}
};

// Match comment
if (str.length > 0) {
// The rest of the string is parsed as comment. Remove "-" if needed.
jsDocTag.description = str.replace(/^\s*-\s*/, "").trim() || undefined;
}
const matchComment = () => {
// Match comment
if (str.length > 0) {
// The rest of the string is parsed as comment. Remove "-" if needed.
jsDocTag.description = str.replace(/^\s*-\s*/, "").trim() || undefined;
}

// Expand the name based on namespace and classname
if (jsDocTag.name != null) {
/**
* The name could look like this, so we need to parse and the remove the class name and namespace from the name
* InputSwitch#[CustomEvent]input-switch-check-changed
* InputSwitch#input-switch-check-changed
*/
const match = jsDocTag.name.match(/(.*)#(\[.*\])?(.*)/);
if (match != null) {
jsDocTag.className = match[1];
jsDocTag.namespace = match[2];
jsDocTag.name = match[3];
// Expand the name based on namespace and classname
if (jsDocTag.name != null) {
/**
* The name could look like this, so we need to parse and the remove the class name and namespace from the name
* InputSwitch#[CustomEvent]input-switch-check-changed
* InputSwitch#input-switch-check-changed
*/
const match = jsDocTag.name.match(/(.*)#(\[.*\])?(.*)/);
if (match != null) {
jsDocTag.className = match[1];
jsDocTag.namespace = match[2];
jsDocTag.name = match[3];
}
}
};

matchTag();
matchType();
matchName();

// Type can come both before and after "name"
if (jsDocTag.type == null) {
matchType();
}

matchComment();

return jsDocTag;
}

Expand Down
2 changes: 1 addition & 1 deletion test/flavors/custom-element/extending-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { tsTest } from "../../helpers/ts-test";
import { analyzeTextWithCurrentTsModule } from "../../helpers/analyze-text-with-current-ts-module";
import { tsTest } from "../../helpers/ts-test";
import { getComponentProp } from "../../helpers/util";

tsTest("Correctly extends interface with interface from different file", t => {
Expand Down
2 changes: 1 addition & 1 deletion test/flavors/custom-element/method-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { tsTest } from "../../helpers/ts-test";
import { analyzeTextWithCurrentTsModule } from "../../helpers/analyze-text-with-current-ts-module";
import { tsTest } from "../../helpers/ts-test";

tsTest("Correctly finds method declarations on a class", t => {
const {
Expand Down
4 changes: 2 additions & 2 deletions test/flavors/jsdoc/csspart-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { tsTest } from "../../helpers/ts-test";
import { analyzeTextWithCurrentTsModule } from "../../helpers/analyze-text-with-current-ts-module";
import { tsTest } from "../../helpers/ts-test";

tsTest("jsdoc: Discovers css parts with @csspart", t => {
const {
Expand All @@ -13,7 +13,7 @@ tsTest("jsdoc: Discovers css parts with @csspart", t => {
}
`);

const { cssParts } = result.componentDefinitions[0].declaration;
const { cssParts } = result.componentDefinitions[0].declaration!;

t.is(cssParts.length, 1);
t.is(cssParts[0].name, "thumb");
Expand Down
2 changes: 1 addition & 1 deletion test/flavors/jsdoc/description-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ tsTest("jsdoc: Correctly discovers the description in the jsdoc", t => {
}
`);

const declaration = result.componentDefinitions[0].declaration;
const declaration = result.componentDefinitions[0].declaration!;

t.is(
declaration.jsDoc?.description,
Expand Down
2 changes: 1 addition & 1 deletion test/flavors/jsdoc/discover-global-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { tsTest } from "../../helpers/ts-test";
import { analyzeTextWithCurrentTsModule } from "../../helpers/analyze-text-with-current-ts-module";
import { tsTest } from "../../helpers/ts-test";

tsTest("jsdoc: Discovers global features on HTMLElement", t => {
const {
Expand Down
2 changes: 1 addition & 1 deletion test/flavors/jsdoc/element-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { tsTest } from "../../helpers/ts-test";
import { analyzeTextWithCurrentTsModule } from "../../helpers/analyze-text-with-current-ts-module";
import { tsTest } from "../../helpers/ts-test";

tsTest("jsdoc: Discovers custom elements with @element", t => {
const {
Expand Down
16 changes: 10 additions & 6 deletions test/flavors/jsdoc/event-test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isAssignableToSimpleTypeKind, SimpleType, SimpleTypeKind } from "ts-simple-type";
import { isAssignableToSimpleTypeKind, SimpleType } from "ts-simple-type";
import { analyzeTextWithCurrentTsModule } from "../../helpers/analyze-text-with-current-ts-module";
import { tsTest } from "../../helpers/ts-test";

Expand All @@ -14,28 +14,32 @@ tsTest("jsdoc: Discovers custom events with @fires", t => {
}
`);

const { events } = result.componentDefinitions[0].declaration;
const { events } = result.componentDefinitions[0].declaration!;

t.is(events.length, 1);
t.is(events[0].name, "my-event");
t.is(events[0].jsDoc?.description, "This is a comment");
t.truthy(isAssignableToSimpleTypeKind(events[0].type() as SimpleType, "ANY"));
});

tsTest("jsdoc: Discovers the detail type of custom events with @fires", t => {
tsTest.only("jsdoc: Discovers the detail type of custom events with @fires", t => {
const {
results: [result]
} = analyzeTextWithCurrentTsModule(`
/**
* @element
* @fires {string} my-event
* @fires my-second-event {number}
*/
class MyElement extends HTMLElement {
}
`);

const { events } = result.componentDefinitions[0].declaration;
t.truthy(isAssignableToSimpleTypeKind(events[0].type() as SimpleType, "STRING"));
const { events } = result.componentDefinitions[0].declaration!;
const myEvent = events.find(e => e.name === "my-event")!;
const mySecondEvent = events.find(e => e.name === "my-second-event")!;
t.truthy(isAssignableToSimpleTypeKind(myEvent.type() as SimpleType, "STRING"));
t.truthy(isAssignableToSimpleTypeKind(mySecondEvent.type() as SimpleType, "NUMBER"));
});

tsTest("jsdoc: Discovers events declared with @fires that includes extra jsdoc information", t => {
Expand All @@ -50,7 +54,7 @@ tsTest("jsdoc: Discovers events declared with @fires that includes extra jsdoc i
}
`);

const { events } = result.componentDefinitions[0].declaration;
const { events } = result.componentDefinitions[0].declaration!;

t.is(events.length, 1);
t.is(events[0].name, "input-switch-check-changed");
Expand Down
4 changes: 2 additions & 2 deletions test/flavors/jsdoc/ignore-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { tsTest } from "../../helpers/ts-test";
import { analyzeTextWithCurrentTsModule } from "../../helpers/analyze-text-with-current-ts-module";
import { tsTest } from "../../helpers/ts-test";

tsTest("jsDoc: Handles @ignore jsdoc tag", t => {
const {
Expand All @@ -26,7 +26,7 @@ tsTest("jsDoc: Handles @ignore jsdoc tag", t => {
}
`);

const { events, methods, members } = result.componentDefinitions[0]?.declaration;
const { events, methods, members } = result.componentDefinitions[0].declaration!;

t.is(events.length, 0);
t.is(members.length, 0);
Expand Down
2 changes: 1 addition & 1 deletion test/flavors/jsdoc/jsdoc-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import test from "ava";
import { SimpleType, SimpleTypeKind } from "ts-simple-type";
import { SimpleType } from "ts-simple-type";
import { parseSimpleJsDocTypeExpression } from "../../../src/analyze/util/js-doc-util";

test("Parse required and union", t => {
Expand Down
1 change: 0 additions & 1 deletion test/flavors/jsdoc/member-test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { SimpleTypeKind } from "ts-simple-type";
import { analyzeTextWithCurrentTsModule } from "../../helpers/analyze-text-with-current-ts-module";
import { tsTest } from "../../helpers/ts-test";
import { assertHasMembers } from "../../helpers/util";
Expand Down
3 changes: 1 addition & 2 deletions test/flavors/jsdoc/modifier-test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { SimpleTypeKind } from "ts-simple-type";
import { tsTest } from "../../helpers/ts-test";
import { analyzeTextWithCurrentTsModule } from "../../helpers/analyze-text-with-current-ts-module";
import { tsTest } from "../../helpers/ts-test";
import { assertHasMembers } from "../../helpers/util";

tsTest("jsDoc: Handles @readonly on members", t => {
Expand Down
8 changes: 4 additions & 4 deletions test/flavors/jsdoc/slot-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { tsTest } from "../../helpers/ts-test";
import { analyzeTextWithCurrentTsModule } from "../../helpers/analyze-text-with-current-ts-module";
import { tsTest } from "../../helpers/ts-test";

tsTest("jsdoc: Discovers slots with @slots", t => {
const {
Expand All @@ -13,7 +13,7 @@ tsTest("jsdoc: Discovers slots with @slots", t => {
}
`);

const { slots } = result.componentDefinitions[0].declaration;
const { slots } = result.componentDefinitions[0].declaration!;

t.is(slots.length, 1);
t.is(slots[0].name, "myslot");
Expand All @@ -32,7 +32,7 @@ tsTest("jsdoc: Discovers unnamed slots with @slots", t => {
}
`);

const { slots } = result.componentDefinitions[0].declaration;
const { slots } = result.componentDefinitions[0].declaration!;

t.is(slots.length, 1);
t.log(slots[0]);
Expand All @@ -55,7 +55,7 @@ tsTest("jsdoc: Discovers permitted tag names on @slot", t => {

const {
slots: [slot1, slot2]
} = result.componentDefinitions[0].declaration;
} = result.componentDefinitions[0].declaration!;

t.is(slot1.permittedTagNames!.length, 2);
t.deepEqual(slot1.permittedTagNames, ["div", "span"]);
Expand Down
4 changes: 2 additions & 2 deletions test/flavors/jsdoc/visibility-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { tsTest } from "../../helpers/ts-test";
import { analyzeTextWithCurrentTsModule } from "../../helpers/analyze-text-with-current-ts-module";
import { tsTest } from "../../helpers/ts-test";
import { getComponentProp } from "../../helpers/util";

tsTest("jsDoc: Handles visibility modifier on internal event", t => {
Expand All @@ -21,7 +21,7 @@ tsTest("jsDoc: Handles visibility modifier on internal event", t => {

const {
events: [event]
} = result.componentDefinitions[0]?.declaration;
} = result.componentDefinitions[0].declaration!;

t.truthy(event);
t.is(event.visibility, "private");
Expand Down
3 changes: 1 addition & 2 deletions test/flavors/jsx/discover-global-test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { SimpleTypeKind } from "ts-simple-type";
import { tsTest } from "../../helpers/ts-test";
import { analyzeTextWithCurrentTsModule } from "../../helpers/analyze-text-with-current-ts-module";
import { tsTest } from "../../helpers/ts-test";
import { assertHasMembers } from "../../helpers/util";

tsTest("Discovers global features on JSX.IntrinsicAttributes", t => {
Expand Down
Loading

0 comments on commit bd0cff3

Please sign in to comment.