Thanks to visit codestin.com
Credit goes to github.com

Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/calm-avocados-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lit-labs/analyzer': patch
---

Supports `@fires {EventType} name`, with type preceding name
5 changes: 5 additions & 0 deletions .changeset/lazy-radios-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lit-labs/analyzer': patch
---

Supports lowercase `@cssprop`, `@cssproperty`, and `@csspart`
5 changes: 5 additions & 0 deletions .changeset/selfish-lions-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lit-labs/analyzer': patch
---

Supports `@cssProperty {<color>} --my-color` with syntax metadata
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import {
NamedDescribed,
} from '../model.js';
import {addEventsToMap} from './events.js';
import {parseNodeJSDocInfo, parseNamedJSDocInfo} from '../javascript/jsdoc.js';
import {
parseNodeJSDocInfo,
parseNamedTypedJSDocInfo,
} from '../javascript/jsdoc.js';

const _isCustomElementClassDeclaration = (
t: ts.BaseType,
Expand Down Expand Up @@ -112,7 +115,7 @@ const addNamedJSDocInfoToMap = (
tag: ts.JSDocTag,
analyzer: AnalyzerInterface
) => {
const info = parseNamedJSDocInfo(tag, analyzer);
const info = parseNamedTypedJSDocInfo(tag, analyzer);
if (info !== undefined) {
map.set(info.name, info);
}
Expand Down Expand Up @@ -141,12 +144,13 @@ export const getJSDocData = (
addNamedJSDocInfoToMap(slots, tag, analyzer);
break;
case 'cssProp':
addNamedJSDocInfoToMap(cssProperties, tag, analyzer);
break;
case 'cssprop':
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we really care about the cases like this, or can we lowercase the tag before hitting the switch statement to get a case-insensitive comparison?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

i initially lowercased the tag, but like ... @cSsPRop...

case 'cssProperty':
case 'cssproperty':
addNamedJSDocInfoToMap(cssProperties, tag, analyzer);
break;
case 'cssPart':
case 'csspart':
addNamedJSDocInfoToMap(cssParts, tag, analyzer);
break;
}
Expand Down
101 changes: 31 additions & 70 deletions packages/labs/analyzer/src/lib/javascript/jsdoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
TypedNamedDescribed,
DeprecatableDescribed,
AnalyzerInterface,
NamedDescribed,
CSSPropertyInfo,
} from '../model.js';

export type TypeScript = typeof ts;
Expand All @@ -43,17 +43,11 @@ const normalizeLineEndings = (s: string) => s.replace(/\r/g, '').trim();

// Regex for parsing name, type, and description from JSDoc comments
const parseNameTypeDescRE =
/^(?<name>\S+)(?:\s+{(?<type>.*)})?(?:\s+-\s+)?(?<description>[\s\S]*)$/m;
/^\[?(?<name>[^{}[\]\s=]+)(?:=(?<defaultValue>[^\]]+))?\]?(?:\s+{(?<type>.*)})?(?:\s+-\s+)?(?<description>[\s\S]*)$/m;

// Regex for parsing optional name and description from JSDoc comments, where
// the dash is required before the description (syntax for `@slot` tag, whose
// default slot has no name)
const parseNameDashDescRE =
/^\[?(?<name>[^[\]\s=]+)(?:=(?<defaultValue>[^\]]+))?\]?\s+-\s+(?<description>[\s\S]*)$/;

// Regex for parsing optional name, default, and description from JSDoc comments
const parseNameDescRE =
/^\[?(?<name>[^[\]\s=]+)(?:=(?<defaultValue>[^\]]+))?\]?(?:\s+-\s+)?(?<description>[\s\S]*)$/;
// Regex for parsing type, name, and description from JSDoc comments
const parseTypeNameDescRE =
/^\{(?<type>.+)\}\s+\[?(?<name>[^{}[\]\s=]+)(?:=(?<defaultValue>[^\]]+))?\]?(?:\s+-\s+)?(?<description>[\s\S]*)$/m;

const getJSDocTagComment = (tag: ts.JSDocTag, analyzer: AnalyzerInterface) => {
let {comment} = tag;
Expand Down Expand Up @@ -93,6 +87,22 @@ const isModuleJSDocTag = (tag: ts.JSDocTag) =>
* * @fires event-name {Type} description
* * @fires event-name {Type} - description
* * @fires event-name {Type}: description
* * @fires {Type} event-name
* * @fires {Type} event-name description
* * @fires {Type} event-name - description
* * @fires {Type} event-name: description
* * @slot name
* * @slot name description
* * @slot name - description
* * @slot name: description
* * @cssProp [--name=default]
* * @cssProp [--name=default] description
* * @cssProp [--name=default] - description
* * @cssProp [--name=default]: description
* * @cssprop {<color>} [--name=default]
* * @cssprop {<color>} [--name=default] description
* * @cssprop {<color>} [--name=default] - description
* * @cssprop {<color>} [--name=default]: description
*/
export const parseNamedTypedJSDocInfo = (
tag: ts.JSDocTag,
Expand All @@ -102,7 +112,9 @@ export const parseNamedTypedJSDocInfo = (
if (comment == undefined) {
return undefined;
}
const nameTypeDesc = comment.match(parseNameTypeDescRE);
const regex =
comment.charAt(0) === '{' ? parseTypeNameDescRE : parseNameTypeDescRE;
const nameTypeDesc = comment.match(regex);
if (nameTypeDesc === null) {
analyzer.addDiagnostic(
createDiagnostic({
Expand All @@ -113,70 +125,19 @@ export const parseNamedTypedJSDocInfo = (
);
return undefined;
}
const {name, type, description} = nameTypeDesc.groups!;
const info: TypedNamedDescribed = {name, type};
const {name, type, defaultValue, description} = nameTypeDesc.groups!;
const info: TypedNamedDescribed = {name};
if (description.length > 0) {
info.description = normalizeLineEndings(description);
}
return info;
};

function makeDashParseDiagnostic(
typescript: TypeScript,
tag: ts.JSDocTag,
requireDash: boolean
) {
return createDiagnostic({
typescript,
node: tag,
message: `Unexpected JSDoc format.${
requireDash
? ' Tag must contain a whitespace-separated dash between the name and description, ' +
"i.e. '@slot header - This is the description'"
: ''
}`,
});
}

/**
* Parses name and description from JSDoc tag for things like `@slot`,
* `@cssPart`, and `@cssProp`.
*
* Supports the following patterns following the tag (TS parses the tag for us):
* * @slot name
* * @slot name description
* * @slot name - description
* * @slot name: description
* * @cssProp [--name=default]
* * @cssProp [--name=default] description
* * @cssProp [--name=default] - description
* * @cssProp [--name=default]: description
*/
export const parseNamedJSDocInfo = (
tag: ts.JSDocTag,
analyzer: AnalyzerInterface,
requireDash = false
): NamedDescribed | undefined => {
const comment = getJSDocTagComment(tag, analyzer);
if (comment == undefined) {
return undefined;
}
const regex = requireDash ? parseNameDashDescRE : parseNameDescRE;
const nameDesc = comment.match(regex);
if (nameDesc === null) {
analyzer.addDiagnostic(
makeDashParseDiagnostic(analyzer.typescript, tag, requireDash)
);
return undefined;
}
const {name, description, defaultValue} = nameDesc.groups!;
const info: NamedDescribed = {name};
if (description?.length > 0) {
info.description = normalizeLineEndings(description);
}
if (defaultValue?.length > 0) {
info.default = defaultValue;
}
if (tag.tagName.text.toLowerCase().startsWith('cssprop')) {
(info as CSSPropertyInfo).syntax = type;
} else {
info.type = type;
}
return info;
};

Expand Down
8 changes: 6 additions & 2 deletions packages/labs/analyzer/src/lib/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,10 @@ export interface NamedDescribed extends Described {
default?: string;
}

export interface CSSPropertyInfo extends NamedDescribed {
syntax?: string;
}

export interface TypedNamedDescribed extends NamedDescribed {
type?: string;
}
Expand All @@ -551,7 +555,7 @@ interface CustomElementDeclarationInit extends ClassDeclarationInit {
tagname: string | undefined;
events: Map<string, Event>;
slots: Map<string, NamedDescribed>;
cssProperties: Map<string, NamedDescribed>;
cssProperties: Map<string, CSSPropertyInfo>;
cssParts: Map<string, NamedDescribed>;
}

Expand All @@ -572,7 +576,7 @@ export class CustomElementDeclaration extends ClassDeclaration {
readonly tagname: string | undefined;
readonly events: Map<string, Event>;
readonly slots: Map<string, NamedDescribed>;
readonly cssProperties: Map<string, NamedDescribed>;
readonly cssProperties: Map<string, CSSPropertyInfo>;
readonly cssParts: Map<string, NamedDescribed>;

constructor(init: CustomElementDeclarationInit) {
Expand Down
93 changes: 92 additions & 1 deletion packages/labs/analyzer/src/test/lit-element/events_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ for (const lang of languages) {
});

test('Correct number of events found', ({element}) => {
assert.equal(element.events.size, 10);
assert.equal(element.events.size, 17);
});

test('Just event name', ({element}) => {
Expand Down Expand Up @@ -63,6 +63,16 @@ for (const lang of languages) {
assert.equal(event.type?.references[0].isGlobal, true);
});

test('Event with well-formed type', ({element}) => {
const event = element.events.get('ordered-typed-event');
assert.ok(event);
assert.equal(event.name, 'ordered-typed-event');
assert.equal(event.type?.text, 'MouseEvent');
assert.equal(event.description, undefined);
assert.equal(event.type?.references[0].name, 'MouseEvent');
assert.equal(event.type?.references[0].isGlobal, true);
});

test('Event with type and description', ({element}) => {
const event = element.events.get('typed-event-two');
assert.ok(event);
Expand All @@ -71,6 +81,14 @@ for (const lang of languages) {
assert.equal(event.description, 'This is a typed event');
});

test('Event with well-formed type and description', ({element}) => {
const event = element.events.get('ordered-typed-event-two');
assert.ok(event);
assert.equal(event.name, 'ordered-typed-event-two');
assert.equal(event.type?.text, 'MouseEvent');
assert.equal(event.description, 'This is a typed event');
});

test('Event with type and dash-separated description', ({element}) => {
const event = element.events.get('typed-event-three');
assert.ok(event);
Expand All @@ -79,6 +97,16 @@ for (const lang of languages) {
assert.equal(event.description, 'This is another typed event');
});

test('Event with well-formed type and dash-separated description', ({
element,
}) => {
const event = element.events.get('ordered-typed-event-three');
assert.ok(event);
assert.equal(event.name, 'ordered-typed-event-three');
assert.equal(event.type?.text, 'MouseEvent');
assert.equal(event.description, 'This is another typed event');
});

test('Event with local custom event type', ({element}) => {
const event = element.events.get('local-custom-event');
assert.ok(event);
Expand All @@ -92,6 +120,19 @@ for (const lang of languages) {
assert.equal(event.description, 'Local custom event');
});

test('Event with local custom event well-formed type', ({element}) => {
const event = element.events.get('ordered-local-custom-event');
assert.ok(event);
assert.equal(event.type?.text, 'LocalCustomEvent');
assert.equal(
event.type?.references[0].package,
'@lit-internal/test-events'
);
assert.equal(event.type?.references[0].module, 'element-a.js');
assert.equal(event.type?.references[0].name, 'LocalCustomEvent');
assert.equal(event.description, 'Local custom event');
});

test('Event with imported custom event type', ({element}) => {
const event = element.events.get('external-custom-event');
assert.ok(event);
Expand All @@ -105,6 +146,19 @@ for (const lang of languages) {
assert.equal(event.description, 'External custom event');
});

test('Event with imported custom event well-formed type', ({element}) => {
const event = element.events.get('ordered-external-custom-event');
assert.ok(event);
assert.equal(event.type?.text, 'ExternalCustomEvent');
assert.equal(
event.type?.references[0].package,
'@lit-internal/test-events'
);
assert.equal(event.type?.references[0].module, 'custom-event.js');
assert.equal(event.type?.references[0].name, 'ExternalCustomEvent');
assert.equal(event.description, 'External custom event');
});

test('Event with generic custom event type', ({element}) => {
const event = element.events.get('generic-custom-event');
assert.ok(event);
Expand All @@ -120,6 +174,21 @@ for (const lang of languages) {
assert.equal(event.description, 'Generic custom event');
});

test('Event with generic custom event well-formed type', ({element}) => {
const event = element.events.get('ordered-generic-custom-event');
assert.ok(event);
assert.equal(event.type?.text, 'CustomEvent<ExternalClass>');
assert.equal(event.type?.references[0].name, 'CustomEvent');
assert.equal(event.type?.references[0].isGlobal, true);
assert.equal(
event.type?.references[1].package,
'@lit-internal/test-events'
);
assert.equal(event.type?.references[1].module, 'custom-event.js');
assert.equal(event.type?.references[1].name, 'ExternalClass');
assert.equal(event.description, 'Generic custom event');
});

test('Event with custom event type with inline detail', ({element}) => {
const event = element.events.get('inline-detail-custom-event');
assert.ok(event);
Expand All @@ -140,5 +209,27 @@ for (const lang of languages) {
assert.equal(event.description, 'Inline\ndetail custom event description');
});

test('Event with custom event well-formed type with inline detail', ({
element,
}) => {
const event = element.events.get('ordered-inline-detail-custom-event');
assert.ok(event);
assert.equal(
event.type?.text,
'CustomEvent<{ event: MouseEvent; more: { impl: ExternalClass; }; }>'
);
assert.equal(event.type?.references[0].name, 'CustomEvent');
assert.equal(event.type?.references[0].isGlobal, true);
assert.equal(event.type?.references[1].name, 'MouseEvent');
assert.equal(event.type?.references[1].isGlobal, true);
assert.equal(
event.type?.references[2].package,
'@lit-internal/test-events'
);
assert.equal(event.type?.references[2].module, 'custom-event.js');
assert.equal(event.type?.references[2].name, 'ExternalClass');
assert.equal(event.description, 'Inline\ndetail custom event description');
});

test.run();
}
Loading