diff --git a/.changeset/chilled-planets-hug.md b/.changeset/chilled-planets-hug.md new file mode 100644 index 00000000..70adfadc --- /dev/null +++ b/.changeset/chilled-planets-hug.md @@ -0,0 +1,6 @@ +--- +"web-component-analyzer-fork": minor +"lit-analyzer-fork": minor +--- + +Support tag names which are static class properties or variables with no-missing-element-type-definition rule" diff --git a/packages/lit-analyzer/src/test/rules/no-missing-element-type-definition.ts b/packages/lit-analyzer/src/test/rules/no-missing-element-type-definition.ts index c310bc37..bb027d00 100644 --- a/packages/lit-analyzer/src/test/rules/no-missing-element-type-definition.ts +++ b/packages/lit-analyzer/src/test/rules/no-missing-element-type-definition.ts @@ -16,7 +16,7 @@ tsTest("'no-missing-element-type-definition' reports diagnostic when element is hasDiagnostic(t, diagnostics, "no-missing-element-type-definition"); }); -tsTest("'no-missing-element-type-definition' reports no diagnostic when element is not in HTMLElementTagNameMap", t => { +tsTest("'no-missing-element-type-definition' reports no diagnostic when element is in HTMLElementTagNameMap", t => { const { diagnostics } = getDiagnostics( ` class MyElement extends HTMLElement { }; @@ -34,3 +34,45 @@ tsTest("'no-missing-element-type-definition' reports no diagnostic when element hasNoDiagnostics(t, diagnostics); }); + +tsTest("'no-missing-element-type-definition' reports no diagnostic when element is in HTMLElementTagNameMap using class property", t => { + const { diagnostics } = getDiagnostics( + ` + class MyElement extends HTMLElement { + static readonly TAG_NAME = "my-element" + }; + customElements.define(MyElement.TAG_NAME, MyElement) + declare global { + interface HTMLElementTagNameMap { + [MyElement.TAG_NAME]: MyElement + } + } + `, + { + rules: { "no-missing-element-type-definition": true } + } + ); + + hasNoDiagnostics(t, diagnostics); +}); + +tsTest("'no-missing-element-type-definition' reports no diagnostic when element is in HTMLElementTagNameMap variable", t => { + const { diagnostics } = getDiagnostics( + ` + const TAG_NAME = "my-element" + class MyElement extends HTMLElement { + }; + customElements.define(MyElement.TAG_NAME, MyElement) + declare global { + interface HTMLElementTagNameMap { + [TAG_NAME]: MyElement + } + } + `, + { + rules: { "no-missing-element-type-definition": true } + } + ); + + hasNoDiagnostics(t, diagnostics); +}); diff --git a/packages/web-component-analyzer/src/analyze/util/ast-util.ts b/packages/web-component-analyzer/src/analyze/util/ast-util.ts index be370d12..9a934a7e 100644 --- a/packages/web-component-analyzer/src/analyze/util/ast-util.ts +++ b/packages/web-component-analyzer/src/analyze/util/ast-util.ts @@ -97,11 +97,16 @@ export function getInterfaceKeys( continue; } + let keyNode = resolvedKey.node; let identifier: Node | undefined; let declaration: Node | undefined; if (ts.isTypeReferenceNode(member.type)) { // { ____: MyButton; } or { ____: namespace.MyButton; } identifier = member.type.typeName; + if (ts.isComputedPropertyName(member.name)) { + // e.g. [MyButton.TAG] : MyButton -> use initial member name node instead of resolved node + keyNode = member.name.expression; + } } else if (ts.isTypeLiteralNode(member.type)) { identifier = undefined; declaration = member.type; @@ -110,7 +115,7 @@ export function getInterfaceKeys( } if (declaration != null || identifier != null) { - extensions.push({ key: String(resolvedKey.value), keyNode: resolvedKey.node, declaration, identifier }); + extensions.push({ key: String(resolvedKey.value), keyNode, declaration, identifier }); } } } diff --git a/packages/web-component-analyzer/src/analyze/util/resolve-node-value.ts b/packages/web-component-analyzer/src/analyze/util/resolve-node-value.ts index c4ee20f1..ffb0f48a 100644 --- a/packages/web-component-analyzer/src/analyze/util/resolve-node-value.ts +++ b/packages/web-component-analyzer/src/analyze/util/resolve-node-value.ts @@ -77,12 +77,14 @@ export function resolveNodeValue(node: Node | undefined, context: Context): { va return resolveNodeValue(node.expression, { ...context, depth }); } - // Resolve initializer value of enum members. - else if (ts.isEnumMember(node)) { + // Resolve initializer value of enum members or class properties. + else if (ts.isEnumMember(node) || ts.isPropertyDeclaration(node)) { if (node.initializer != null) { return resolveNodeValue(node.initializer, { ...context, depth }); - } else { + } else if (node.parent.name) { return { value: `${node.parent.name.text}.${node.name.getText()}`, node }; + } else { + return { value: `${node.name.getText()}`, node }; } } diff --git a/packages/web-component-analyzer/test/util/resolve-node-value.spec.ts b/packages/web-component-analyzer/test/util/resolve-node-value.spec.ts index c1d27264..ccc5e738 100644 --- a/packages/web-component-analyzer/test/util/resolve-node-value.spec.ts +++ b/packages/web-component-analyzer/test/util/resolve-node-value.spec.ts @@ -52,3 +52,29 @@ type AliasedLiteral = StringLiteral; t.is(actualValue, "popsicles", `Resolved value for '${name.getText()}' is invalid`); }); }); + +test("resolveNodeValue resolves class properties", t => { + const { + analyzedSourceFiles: [sourceFile], + program + } = analyzeText(` +class FooClass { + static readonly FOO_BAR = "foo-bar"; + readonly barBaz = "bar-baz"; + fooBaz = "foo-baz"; +} + `); + + const checker = program.getTypeChecker(); + + const assertPropertyNodeValue = (node: ts.ClassElement, expectedValue: string) => { + const propertyNodeValue = resolveNodeValue(node, { checker, ts })?.value; + t.is(propertyNodeValue, expectedValue, `Resolved value for '${node.name?.getText()}' is invalid`); + }; + + findChildren(sourceFile, ts.isClassDeclaration, ({ name, members }) => { + assertPropertyNodeValue(members[0], "foo-bar"); + assertPropertyNodeValue(members[1], "bar-baz"); + assertPropertyNodeValue(members[2], "foo-baz"); + }); +});