diff --git a/.changeset/quick-coins-joke.md b/.changeset/quick-coins-joke.md
new file mode 100644
index 00000000..e542efc9
--- /dev/null
+++ b/.changeset/quick-coins-joke.md
@@ -0,0 +1,5 @@
+---
+"@jackolope/lit-analyzer": patch
+---
+
+fix: Take the presence of converter functions into account for attributes with no-complex-attribute-binding and no-incompatible-type-binding rules
diff --git a/packages/lit-analyzer/src/lib/rules/no-complex-attribute-binding.ts b/packages/lit-analyzer/src/lib/rules/no-complex-attribute-binding.ts
index 84eca74a..c9195906 100644
--- a/packages/lit-analyzer/src/lib/rules/no-complex-attribute-binding.ts
+++ b/packages/lit-analyzer/src/lib/rules/no-complex-attribute-binding.ts
@@ -28,6 +28,9 @@ const rule: RuleModule = {
// Don't validate directives in this rule, because they are assignable even though they are complex types (functions).
if (isLitDirective(typeB)) return;
+ const htmlAttrTarget = context.htmlStore.getHtmlAttrTarget(htmlAttr);
+ const hasConverter = htmlAttrTarget?.declaration?.meta?.hasConverter;
+
// Only primitive types should be allowed as "typeB"
if (!isAssignableToPrimitiveType(typeB)) {
if (isAssignableBindingUnderSecuritySystem(htmlAttr, { typeA, typeB }, context) !== undefined) {
@@ -56,8 +59,8 @@ const rule: RuleModule = {
});
}
- // Only primitive types should be allowed as "typeA"
- else if (!isAssignableToPrimitiveType(typeA)) {
+ // Only primitive types without a custom converter should be allowed as "typeA"
+ else if (!hasConverter && !isAssignableToPrimitiveType(typeA)) {
const message = `You are assigning the primitive '${typeToString(typeB)}' to a non-primitive type '${typeToString(typeA)}'.`;
const newModifier = ".";
diff --git a/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-attribute-binding.ts b/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-attribute-binding.ts
index d1d515e8..896c2f6e 100644
--- a/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-attribute-binding.ts
+++ b/packages/lit-analyzer/src/lib/rules/util/type/is-assignable-in-attribute-binding.ts
@@ -9,6 +9,7 @@ import { isPrimitiveArrayType } from "../../../analyze/util/type-util.js";
import { isLitDirective } from "../directive/is-lit-directive.js";
import { isAssignableBindingUnderSecuritySystem } from "./is-assignable-binding-under-security-system.js";
import { isAssignableToType } from "./is-assignable-to-type.js";
+import { HtmlNodeAttrKind } from "../../../analyze/types/html-node/html-node-attr-types.js";
export function isAssignableInAttributeBinding(
htmlAttr: HtmlNodeAttr,
@@ -18,6 +19,16 @@ export function isAssignableInAttributeBinding(
const { assignment } = htmlAttr;
if (assignment == null) return undefined;
+ // If the attribute has a custom converter, then do not check it
+ if (htmlAttr.kind === HtmlNodeAttrKind.ATTRIBUTE) {
+ const htmlAttrTarget = context.htmlStore.getHtmlAttrTarget(htmlAttr);
+ const hasConverter = htmlAttrTarget?.declaration?.meta?.hasConverter;
+
+ if (hasConverter) {
+ return undefined;
+ }
+ }
+
if (assignment.kind === HtmlNodeAttrAssignmentKind.BOOLEAN) {
if (!isAssignableToType({ typeA, typeB }, context)) {
context.report({
diff --git a/packages/lit-analyzer/src/test/helpers/generate-test-file.ts b/packages/lit-analyzer/src/test/helpers/generate-test-file.ts
index 104631d1..fb6aaa89 100644
--- a/packages/lit-analyzer/src/test/helpers/generate-test-file.ts
+++ b/packages/lit-analyzer/src/test/helpers/generate-test-file.ts
@@ -1,6 +1,28 @@
import type { TestFile } from "./compile-files.js";
-export function makeElement({ properties, slots }: { properties?: string[]; slots?: string[] }): TestFile {
+/**
+ * Allows you to configure and test options that you would provide to Lit's @property decorator by setting `fullPropertyDeclaration` to true.
+ * @link https://lit.dev/docs/api/ReactiveElement/#PropertyDeclaration
+ * Some of these options are used in the analyzer, and are set in the `parse-lit-property-configuration` file in the web-component-analyzer-package.
+ * They are available on the `.meta` field.
+ */
+export function makeElement({
+ properties,
+ slots,
+ fullPropertyDeclaration
+}: {
+ properties?: string[];
+ slots?: string[];
+ fullPropertyDeclaration?: boolean;
+}): TestFile {
+ let propertiesString: string | undefined;
+
+ if (fullPropertyDeclaration) {
+ propertiesString = properties?.join("\n");
+ } else {
+ propertiesString = properties?.map(prop => `@property() ${prop}`).join("\n");
+ }
+
return {
fileName: "my-element.ts",
text: `
@@ -8,7 +30,7 @@ export function makeElement({ properties, slots }: { properties?: string[]; slot
${(slots || []).map(slot => ` * @slot ${slot}`)}
*/
class MyElement extends HTMLElement {
- ${(properties || []).map(prop => `@property() ${prop}`).join("\n")}
+ ${propertiesString}
};
customElements.define("my-element", MyElement);
`
diff --git a/packages/lit-analyzer/src/test/rules/no-complex-attribute-binding.ts b/packages/lit-analyzer/src/test/rules/no-complex-attribute-binding.ts
index 90dce5ef..42e0135f 100644
--- a/packages/lit-analyzer/src/test/rules/no-complex-attribute-binding.ts
+++ b/packages/lit-analyzer/src/test/rules/no-complex-attribute-binding.ts
@@ -37,3 +37,27 @@ tsTest("Ignore element expressions", t => {
const { diagnostics } = getDiagnostics("html``", { rules: { "no-incompatible-type-binding": false } });
hasNoDiagnostics(t, diagnostics);
});
+
+tsTest("Complex types are assignable to attributes using converters", t => {
+ const { diagnostics } = getDiagnostics(
+ [
+ makeElement({
+ properties: [
+ `@property({ converter: {
+ fromAttribute(str) { return str.split(','); },
+ toAttribute(arr) { return arr.join(','); }
+ }})
+ complex: string[];`
+ ],
+ fullPropertyDeclaration: true
+ }),
+ 'html``'
+ ],
+ {
+ rules: {
+ "no-incompatible-type-binding": "off"
+ }
+ }
+ );
+ hasNoDiagnostics(t, diagnostics);
+});
diff --git a/packages/lit-analyzer/src/test/rules/no-incompatible-type-binding.ts b/packages/lit-analyzer/src/test/rules/no-incompatible-type-binding.ts
index 30789bde..80b59f5e 100644
--- a/packages/lit-analyzer/src/test/rules/no-incompatible-type-binding.ts
+++ b/packages/lit-analyzer/src/test/rules/no-incompatible-type-binding.ts
@@ -294,6 +294,23 @@ tsTest("Attribute binding: the target attribute is correctly type checked when g
hasNoDiagnostics(t, diagnostics);
});
+tsTest("Strings are assignable to types with converters", t => {
+ const { diagnostics } = getDiagnostics([
+ makeElement({
+ properties: [
+ `@property({ converter: {
+ fromAttribute(str) { return str.split(','); },
+ toAttribute(arr) { return arr.join(','); }
+ }})
+ complex: string[];`
+ ],
+ fullPropertyDeclaration: true
+ }),
+ 'html``'
+ ]);
+ hasNoDiagnostics(t, diagnostics);
+});
+
tsTest("Attribute binding: any symbols are ignored on type checking", t => {
const { diagnostics } = getDiagnostics(`
declare const value: boolean | unique symbol;