diff --git a/dev/src/my-element-1.ts b/dev/src/my-element-1.ts index 9afe595a..574caae9 100644 --- a/dev/src/my-element-1.ts +++ b/dev/src/my-element-1.ts @@ -13,8 +13,11 @@ export class MyElement extends LitElement { return ["this is a test", "testing"]; } + didClick(evt: MouseEvent) {} + render() { return html` + diff --git a/docs/readme/rules.md b/docs/readme/rules.md index 2c1cd08c..a64e3caf 100644 --- a/docs/readme/rules.md +++ b/docs/readme/rules.md @@ -31,7 +31,6 @@ Each rule can have severity of `off`, `warning` or `error`. You can toggle rules | :------ | ----------- | --------------- | --------------- | | [no-invalid-boolean-binding](#-no-invalid-boolean-binding) | Disallow boolean attribute bindings on non-boolean types. | error | error | | [no-expressionless-property-binding](#-no-expressionless-property-binding) | Disallow property bindings without an expression. | error | error | -| [no-noncallable-event-binding](#-no-noncallable-event-binding) | Disallow event listener bindings with a noncallable type. | error | error | | [no-boolean-in-attribute-binding](#-no-boolean-in-attribute-binding) | Disallow attribute bindings with a boolean type. | error | error | | [no-complex-attribute-binding](#-no-complex-attribute-binding) | Disallow attribute bindings with a complex type. | error | error | | [no-nullable-attribute-binding](#-no-nullable-attribute-binding) | Disallow attribute bindings with nullable types such as "null" or "undefined". | error | error | @@ -296,26 +295,6 @@ The following example is not considered a warning: html`` ``` -#### 🌀 no-noncallable-event-binding - -It's a common mistake to incorrectly call the function when setting up an event handler binding instead of passing a reference to the function. This makes the function call whenever the code evaluates. - -The following examples are considered warnings: - - -```js -html`` -html`` -``` - -The following examples are not considered warnings: - - -```js -html`` -html`` -``` - #### 😈 no-boolean-in-attribute-binding You should not be binding to a boolean type using an attribute binding because it could result in binding the string "true" or "false". Instead you should be using a **boolean** attribute binding. @@ -386,6 +365,8 @@ html`` html`` html`` html`` +html`` +html`` ``` The following examples are not considered warnings: @@ -396,6 +377,8 @@ html`` html`` html`` html`` +html`` +html`` ``` #### 💥 no-invalid-directive-binding diff --git a/packages/lit-analyzer/README.md b/packages/lit-analyzer/README.md index b4f436fb..bd0e2ff6 100644 --- a/packages/lit-analyzer/README.md +++ b/packages/lit-analyzer/README.md @@ -104,7 +104,6 @@ Each rule can have severity of `off`, `warning` or `error`. You can toggle rules | :------ | ----------- | --------------- | --------------- | | [no-invalid-boolean-binding](#-no-invalid-boolean-binding) | Disallow boolean attribute bindings on non-boolean types. | error | error | | [no-expressionless-property-binding](#-no-expressionless-property-binding) | Disallow property bindings without an expression. | error | error | -| [no-noncallable-event-binding](#-no-noncallable-event-binding) | Disallow event listener bindings with a noncallable type. | error | error | | [no-boolean-in-attribute-binding](#-no-boolean-in-attribute-binding) | Disallow attribute bindings with a boolean type. | error | error | | [no-complex-attribute-binding](#-no-complex-attribute-binding) | Disallow attribute bindings with a complex type. | error | error | | [no-nullable-attribute-binding](#-no-nullable-attribute-binding) | Disallow attribute bindings with nullable types such as "null" or "undefined". | error | error | @@ -369,26 +368,6 @@ The following example is not considered a warning: html`` ``` -#### 🌀 no-noncallable-event-binding - -It's a common mistake to incorrectly call the function when setting up an event handler binding instead of passing a reference to the function. This makes the function call whenever the code evaluates. - -The following examples are considered warnings: - - -```js -html`` -html`` -``` - -The following examples are not considered warnings: - - -```js -html`` -html`` -``` - #### 😈 no-boolean-in-attribute-binding You should not be binding to a boolean type using an attribute binding because it could result in binding the string "true" or "false". Instead you should be using a **boolean** attribute binding. @@ -459,6 +438,8 @@ html`` html`` html`` html`` +html`` +html`` ``` The following examples are not considered warnings: @@ -469,6 +450,8 @@ html`` html`` html`` html`` +html`` +html`` ``` #### 💥 no-invalid-directive-binding diff --git a/packages/lit-analyzer/src/analyze/default-lit-analyzer-context.ts b/packages/lit-analyzer/src/analyze/default-lit-analyzer-context.ts index 3ec8a1c8..2a4798e2 100644 --- a/packages/lit-analyzer/src/analyze/default-lit-analyzer-context.ts +++ b/packages/lit-analyzer/src/analyze/default-lit-analyzer-context.ts @@ -227,7 +227,7 @@ export class DefaultLitAnalyzerContext implements LitAnalyzerContext { ts: this.ts, config: { features: ["event", "member", "slot", "csspart", "cssproperty"], - analyzeGlobalFeatures: !isDefaultLibrary, // Don't analyze global features in lib.dom.d.ts + analyzeGlobalFeatures: true, analyzeDefaultLib: true, analyzeDependencies: true, analyzeAllDeclarations: false, @@ -235,6 +235,13 @@ export class DefaultLitAnalyzerContext implements LitAnalyzerContext { } }); + // Don't analyze global members in lib.dom.d.ts for now + // We already merge in content from "HTMLElement", but in the future we might + // only use "globalFeatures" instead + if (isDefaultLibrary && analyzeResult.globalFeatures != null) { + analyzeResult.globalFeatures.members = []; + } + const reg = isDefaultLibrary ? HtmlDataSourceKind.BUILT_IN_DECLARED : HtmlDataSourceKind.DECLARED; // Forget diff --git a/packages/lit-analyzer/src/analyze/lit-analyzer-config.ts b/packages/lit-analyzer/src/analyze/lit-analyzer-config.ts index 5c590fd3..c24acfb8 100644 --- a/packages/lit-analyzer/src/analyze/lit-analyzer-config.ts +++ b/packages/lit-analyzer/src/analyze/lit-analyzer-config.ts @@ -14,7 +14,6 @@ export type LitAnalyzerRuleId = | "no-unintended-mixed-binding" | "no-invalid-boolean-binding" | "no-expressionless-property-binding" - | "no-noncallable-event-binding" | "no-boolean-in-attribute-binding" | "no-complex-attribute-binding" | "no-nullable-attribute-binding" @@ -45,7 +44,6 @@ const DEFAULT_RULES_SEVERITY: Record): LitA if (getDeprecatedOption(userOptions, "skipTypeChecking") === true) { Object.assign(mappedDeprecatedRules, { "no-invalid-boolean-binding": "off", - "no-noncallable-event-binding": "off", "no-boolean-in-attribute-binding": "off", "no-complex-attribute-binding": "off", "no-nullable-attribute-binding": "off", diff --git a/packages/lit-analyzer/src/analyze/parse/parse-html-data/html-tag.ts b/packages/lit-analyzer/src/analyze/parse/parse-html-data/html-tag.ts index 8bbf4fe8..3726410a 100644 --- a/packages/lit-analyzer/src/analyze/parse/parse-html-data/html-tag.ts +++ b/packages/lit-analyzer/src/analyze/parse/parse-html-data/html-tag.ts @@ -2,6 +2,7 @@ import { isAssignableToSimpleTypeKind, SimpleType, typeToString } from "ts-simpl import { ComponentCssPart, ComponentCssProperty, ComponentDeclaration, ComponentEvent, ComponentMember, ComponentSlot } from "web-component-analyzer"; import { LIT_HTML_BOOLEAN_ATTRIBUTE_MODIFIER, LIT_HTML_EVENT_LISTENER_ATTRIBUTE_MODIFIER, LIT_HTML_PROP_ATTRIBUTE_MODIFIER } from "../../constants"; import { iterableDefined } from "../../util/iterable-util"; +import { lazy } from "../../util/general-util"; export interface HtmlDataFeatures { attributes: HtmlAttr[]; @@ -188,7 +189,10 @@ export function documentationForHtmlTag(htmlTag: HtmlTag, options: DescriptionOp desc += `\n\n${descriptionList( "Events", items, - event => `${descriptionHeader(`@fires ${event.name}`, 0, options)}${event.description ? ` - ${event.description}` : ""}`, + event => + `${descriptionHeader(`@fires ${typeToString(event.getType())} ${event.name}`, 0, options)}${ + event.description ? ` - ${event.description}` : "" + }`, options )}`; } @@ -256,7 +260,31 @@ export function mergeHtmlProps(props: HtmlProp[]): HtmlProp[] { } export function mergeHtmlEvents(events: HtmlEvent[]): HtmlEvent[] { - return mergeFirstUnique(events, event => event.name); + if (events.length <= 1) { + return events; + } + + const mergedEvents = new Map(); + for (const evt of events) { + const existingEvent = mergedEvents.get(evt.name); + if (existingEvent != null) { + mergedEvents.set(evt.name, { + ...evt, + declaration: existingEvent.declaration || evt.declaration, + fromTagName: existingEvent.fromTagName || evt.fromTagName, + builtIn: existingEvent.builtIn || evt.builtIn, + global: existingEvent.global || evt.global, + description: existingEvent.description || evt.description, + getType: lazy(() => { + const type = existingEvent.getType(); + return type.kind === "ANY" ? evt.getType() : type; + }) + }); + } else { + mergedEvents.set(evt.name, evt); + } + } + return Array.from(mergedEvents.values()); } export function mergeHtmlSlots(slots: HtmlSlot[]): HtmlSlot[] { diff --git a/packages/lit-analyzer/src/analyze/store/html-store/html-data-source-merged.ts b/packages/lit-analyzer/src/analyze/store/html-store/html-data-source-merged.ts index ecf46b9e..3c6d3173 100644 --- a/packages/lit-analyzer/src/analyze/store/html-store/html-data-source-merged.ts +++ b/packages/lit-analyzer/src/analyze/store/html-store/html-data-source-merged.ts @@ -458,24 +458,34 @@ function mergeRelatedMembers(members: Iterable): Readon return mergedMembers; } -function mergeRelatedTypeToUnion(typeA: SimpleType, typeB: SimpleType): SimpleType { +function mergeRelatedTypeToUnion(typeA: SimpleType, typeB: SimpleType, { collapseAny = true }: { collapseAny?: boolean } = {}): SimpleType { if (typeA.kind === typeB.kind) { - switch (typeA.kind) { - case "ANY": - return typeA; + return typeA; + } + + if (collapseAny) { + if (typeB.kind === "ANY") { + return typeA; + } else if (typeA.kind === "ANY") { + return typeB; } } - switch (typeA.kind) { - case "UNION": - if (typeB.kind === "ANY" && typeA.types.find(t => t.kind === "ANY") != null) { + if (typeA.kind === "UNION" && typeB.kind === "UNION") { + if (collapseAny) { + if (typeA.types.some(t => t.kind === "ANY")) { + return typeB; + } + + if (typeB.types.some(t => t.kind === "ANY")) { return typeA; - } else { - return { - ...typeA, - types: [...typeA.types, typeB] - }; } + } + + return { + ...typeA, + types: Array.from(new Set([...typeA.types, ...typeB.types])) + }; } return { @@ -533,7 +543,7 @@ function mergeRelatedEvents(events: Iterable): ReadonlyMap mergeRelatedTypeToUnion(prevType(), event.getType())), + getType: lazy(() => mergeRelatedTypeToUnion(prevType(), event.getType(), { collapseAny: false })), related: existingEvent.related == null ? [existingEvent, event] : [...existingEvent.related, event], fromTagName: existingEvent.fromTagName || event.fromTagName }); diff --git a/packages/lit-analyzer/src/rules/all-rules.ts b/packages/lit-analyzer/src/rules/all-rules.ts index 3e0b567f..66e77ab7 100644 --- a/packages/lit-analyzer/src/rules/all-rules.ts +++ b/packages/lit-analyzer/src/rules/all-rules.ts @@ -10,7 +10,6 @@ import noInvalidTagName from "./no-invalid-tag-name"; import noLegacyAttribute from "./no-legacy-attribute"; import noMissingElementTypeDefinition from "./no-missing-element-type-definition"; import noMissingImport from "./no-missing-import"; -import noNoncallableEventBindingRule from "./no-noncallable-event-binding"; import noNullableAttributeBindingRule from "./no-nullable-attribute-binding"; import noPropertyVisibilityMismatch from "./no-property-visibility-mismatch"; import noUnclosedTag from "./no-unclosed-tag"; @@ -25,7 +24,6 @@ export const ALL_RULES: RuleModule[] = [ noExpressionlessPropertyBindingRule, noUnintendedMixedBindingRule, noUnknownSlotRule, - noNoncallableEventBindingRule, noNullableAttributeBindingRule, noComplexAttributeBindingRule, noBooleanInAttributeBindingRule, diff --git a/packages/lit-analyzer/src/rules/no-incompatible-type-binding.ts b/packages/lit-analyzer/src/rules/no-incompatible-type-binding.ts index 016b0605..d4fb2e7e 100644 --- a/packages/lit-analyzer/src/rules/no-incompatible-type-binding.ts +++ b/packages/lit-analyzer/src/rules/no-incompatible-type-binding.ts @@ -8,6 +8,7 @@ import { extractBindingTypes } from "./util/type/extract-binding-types"; import { isAssignableInAttributeBinding } from "./util/type/is-assignable-in-attribute-binding"; import { isAssignableInBooleanBinding } from "./util/type/is-assignable-in-boolean-binding"; import { isAssignableInPropertyBinding } from "./util/type/is-assignable-in-property-binding"; +import { isAssignableInEventBinding } from "./util/type/is-assignable-in-event-binding"; /** * This rule validate if the types of a binding are assignable. @@ -37,6 +38,7 @@ const rule: RuleModule = { break; case LIT_HTML_EVENT_LISTENER_ATTRIBUTE_MODIFIER: + isAssignableInEventBinding(htmlAttr, { typeA, typeB }, context); break; default: { diff --git a/packages/lit-analyzer/src/rules/no-noncallable-event-binding.ts b/packages/lit-analyzer/src/rules/no-noncallable-event-binding.ts deleted file mode 100644 index 6e8cb0f4..00000000 --- a/packages/lit-analyzer/src/rules/no-noncallable-event-binding.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { isAssignableToSimpleTypeKind, SimpleType, typeToString, validateType } from "ts-simple-type"; -import { HtmlNodeAttrKind } from "../analyze/types/html-node/html-node-attr-types"; -import { RuleModule } from "../analyze/types/rule/rule-module"; -import { rangeFromHtmlNodeAttr } from "../analyze/util/range-util"; -import { extractBindingTypes } from "./util/type/extract-binding-types"; - -/** - * This rule validates that only callable types are used within event binding expressions. - * This rule catches typos like: @click="onClick()" - */ -const rule: RuleModule = { - id: "no-noncallable-event-binding", - meta: { - priority: "high" - }, - visitHtmlAssignment(assignment, context) { - // Only validate event listener bindings. - const { htmlAttr } = assignment; - if (htmlAttr.kind !== HtmlNodeAttrKind.EVENT_LISTENER) return; - - const { typeB } = extractBindingTypes(assignment, context); - - // Make sure that the expression given to the event listener binding a function or an object with "handleEvent" property. - if (!isTypeBindableToEventListener(typeB)) { - context.report({ - location: rangeFromHtmlNodeAttr(htmlAttr), - message: `You are setting up an event listener with a non-callable type '${typeToString(typeB)}'` - }); - } - } -}; - -export default rule; - -/** - * Returns if this type can be used in a event listener binding - * @param type - */ -function isTypeBindableToEventListener(type: SimpleType): boolean { - // Return "true" if the type has a call signature - if ("call" in type && type.call != null) { - return true; - } - - // Callable types can be used in the binding - if (isAssignableToSimpleTypeKind(type, ["FUNCTION", "METHOD", "UNKNOWN"], { matchAny: true })) { - return true; - } - - return validateType(type, simpleType => { - switch (simpleType.kind) { - // Object types with attributes for the setup function of the event listener can be used - case "OBJECT": - case "INTERFACE": { - // The "handleEvent" property must be present - const handleEventFunction = simpleType.members != null ? simpleType.members.find(m => m.name === "handleEvent") : undefined; - - // The "handleEvent" property must be callable - if (handleEventFunction != null) { - return isTypeBindableToEventListener(handleEventFunction.type); - } - } - } - - return undefined; - }); -} diff --git a/packages/lit-analyzer/src/rules/util/type/is-assignable-in-event-binding.ts b/packages/lit-analyzer/src/rules/util/type/is-assignable-in-event-binding.ts new file mode 100644 index 00000000..8fe69092 --- /dev/null +++ b/packages/lit-analyzer/src/rules/util/type/is-assignable-in-event-binding.ts @@ -0,0 +1,129 @@ +import { isAssignableToSimpleTypeKind, SimpleType, typeToString, validateType } from "ts-simple-type"; +import { parseSimpleJsDocTypeExpression } from "web-component-analyzer"; +import { HtmlNodeAttr } from "../../../analyze/types/html-node/html-node-attr-types"; +import { RuleModuleContext } from "../../../analyze/types/rule/rule-module-context"; +import { rangeFromHtmlNodeAttr } from "../../../analyze/util/range-util"; +import { isAssignableToType } from "./is-assignable-to-type"; + +export function isAssignableInEventBinding( + htmlAttr: HtmlNodeAttr, + { typeA, typeB }: { typeA: SimpleType; typeB: SimpleType }, + context: RuleModuleContext +): boolean | undefined { + // Make sure that the expression given to the event listener binding a function or an object with "handleEvent" property. + // This catches typos like: @click="onClick()" + if (!isTypeBindableToEventListener(typeB)) { + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `You are setting up an event listener with a non-callable type '${typeToString(typeB)}'` + }); + + return false; + } + + return isEventAssignable(htmlAttr, { typeA, typeB }, context); +} + +export function isEventAssignable( + htmlAttr: HtmlNodeAttr, + { typeA, typeB }: { typeA: SimpleType; typeB: SimpleType }, + context: RuleModuleContext +): boolean | undefined { + // Treat type "ANY" as "Event" + if (typeA.kind === "ANY") { + typeA = parseSimpleJsDocTypeExpression("Event", context); + } + + // Construct an event handler type to perform type checking against + const expectedType: SimpleType = { + kind: "OBJECT", + call: { + kind: "FUNCTION", + returnType: { kind: "VOID" }, + parameters: [ + { + name: "event", + type: typeA, + optional: false, + rest: false, + initializer: false + } + ] + } + }; + + // Extract the type of "handleEvent" from typeB if an object was given to the event binding + switch (typeB.kind) { + case "OBJECT": + case "INTERFACE": { + const handleEventMember = typeB.members != null ? typeB.members.find(m => m.name === "handleEvent") : undefined; + if (handleEventMember != null) { + typeB = handleEventMember.type; + } + break; + } + } + + let assignable: boolean; + if (typeA.kind === "UNION") { + // If events have been merged into one UNION type, check each event type separately + const mutedContext = { ...context, report() {} }; + assignable = typeA.types.some(tA => isAssignableInEventBinding(htmlAttr, { typeA: tA, typeB }, mutedContext)); + } else { + // We don't always know the exact type of event required. Therefore always type check + // the parameters bivariant instead of contravariant. We primarily care if the parameter in + // typeB is an Event (or something more specific), and this is achieved forcing "strictFunctionTypes" + // to false (to have bivariant type checking for parameters) + // Examples on invalid error message we get if "strictFunctionTypes" is true: + // Type '(event: MouseEvent) => void' is not assignable to '(event: Event) => void' + // Type '(event: CustomEvent) => void' is not assignable to '(event: Event) => void' + // In theory, we would be able to set "strictFunctionTypes: true" if we knew the exact type required, + // but under many circumstances we don't know exactly + const strictFunctionTypes = false; + + assignable = isAssignableToType({ typeA: expectedType, typeB }, context, { strictFunctionTypes }); + } + + if (!assignable) { + context.report({ + location: rangeFromHtmlNodeAttr(htmlAttr), + message: `Type '${typeToString(typeB)}' is not assignable to '${typeToString(expectedType)}'` + }); + } + + return assignable; +} + +/** + * Returns if this type can be used in a event listener binding + * @param type + */ +function isTypeBindableToEventListener(type: SimpleType): boolean { + // Return "true" if the type has a call signature + if ("call" in type && type.call != null) { + return true; + } + + // Callable types can be used in the binding + if (isAssignableToSimpleTypeKind(type, ["FUNCTION", "METHOD", "UNKNOWN"], { matchAny: true })) { + return true; + } + + return validateType(type, simpleType => { + switch (simpleType.kind) { + // Object types with attributes for the setup function of the event listener can be used + case "OBJECT": + case "INTERFACE": { + // The "handleEvent" property must be present + const handleEventMember = simpleType.members != null ? simpleType.members.find(m => m.name === "handleEvent") : undefined; + + // The "handleEvent" property must be callable + if (handleEventMember != null) { + return isTypeBindableToEventListener(handleEventMember.type); + } + } + } + + return undefined; + }); +} diff --git a/packages/lit-analyzer/test/helpers/generate-test-file.ts b/packages/lit-analyzer/test/helpers/generate-test-file.ts index c7d6b951..f2b9794a 100644 --- a/packages/lit-analyzer/test/helpers/generate-test-file.ts +++ b/packages/lit-analyzer/test/helpers/generate-test-file.ts @@ -1,11 +1,12 @@ import { TestFile } from "./compile-files"; -export function makeElement({ properties, slots }: { properties?: string[]; slots?: string[] }): TestFile { +export function makeElement({ properties, slots, events }: { properties?: string[]; slots?: string[]; events?: string[] }): TestFile { return { fileName: "my-element.ts", text: ` /** ${(slots || []).map(slot => ` * @slot ${slot}`)} +${(events || []).map(event => ` * @fires ${event}`)} */ class MyElement extends HTMLElement { ${(properties || []).map(prop => `@property() ${prop}`).join("\n")} diff --git a/packages/lit-analyzer/test/rules/no-incompatible-type-binding.ts b/packages/lit-analyzer/test/rules/no-incompatible-type-binding.ts index 29ecfa55..8791fcdb 100644 --- a/packages/lit-analyzer/test/rules/no-incompatible-type-binding.ts +++ b/packages/lit-analyzer/test/rules/no-incompatible-type-binding.ts @@ -224,3 +224,120 @@ html\` \` hasDiagnostic(t, diagnostics, "no-incompatible-type-binding"); }); + +tsTest("Event binding: event handler is assignable to valid event", t => { + const { diagnostics } = getDiagnostics([makeElement({ events: ["foo-event"] }), "html` {}}>`"]); + hasNoDiagnostics(t, diagnostics); +}); + +tsTest("Event binding: event handler is assignable to valid typed event", t => { + const { diagnostics } = getDiagnostics([ + makeElement({ events: ["{MouseEvent} foo-event"] }), + "html` {}}>`" + ]); + hasNoDiagnostics(t, diagnostics); +}); + +tsTest("Event binding: invalid event handler is not assignable to typed event", t => { + const { diagnostics } = getDiagnostics([ + makeElement({ events: ["{MouseEvent} foo-event"] }), + "html` {}}>`" + ]); + hasDiagnostic(t, diagnostics, "no-incompatible-type-binding"); +}); + +tsTest("Event binding: invalid event handler is not assignable to event", t => { + const { diagnostics } = getDiagnostics([ + makeElement({ events: ["foo-event"] }), + "html` {}}>`" + ]); + hasDiagnostic(t, diagnostics, "no-incompatible-type-binding"); +}); + +tsTest("Event binding: invalid event handler (generic custom event) is not assignable to typed event", t => { + const { diagnostics } = getDiagnostics([ + makeElement({ events: ["{CustomEvent} foo-event"] }), + "html`) => {}}>`" + ]); + hasDiagnostic(t, diagnostics, "no-incompatible-type-binding"); +}); + +tsTest("Event binding: event handler (generic custom event) is assignable to typed event", t => { + const { diagnostics } = getDiagnostics([ + makeElement({ events: ["{CustomEvent} foo-event"] }), + "html`) => {}}>`" + ]); + hasNoDiagnostics(t, diagnostics); +}); + +tsTest("Event binding: event handler is assignable to event with unknown type", t => { + const { diagnostics } = getDiagnostics([ + makeElement({ events: ["foo-event"] }), + "html` {}}>`" + ]); + hasNoDiagnostics(t, diagnostics); +}); + +tsTest("Event binding: event handler is assignable to merged event", t => { + const { diagnostics } = getDiagnostics([ + makeElement({ events: ["{MouseEvent} foo-event"] }), + makeElement({ events: ["{KeyboardEvent} foo-event"] }), + "html` {}}>`" + ]); + hasNoDiagnostics(t, diagnostics); +}); + +tsTest("Event binding: Callable value is bindable", t => { + const { diagnostics } = getDiagnostics('html``'); + hasNoDiagnostics(t, diagnostics); +}); + +tsTest("Event binding: Non callback value is not bindable", t => { + const { diagnostics } = getDiagnostics('html``'); + hasDiagnostic(t, diagnostics, "no-incompatible-type-binding"); +}); + +tsTest("Event binding: Number is not bindable", t => { + const { diagnostics } = getDiagnostics('html``'); + hasDiagnostic(t, diagnostics, "no-incompatible-type-binding"); +}); + +tsTest("Event binding: Function is bindable", t => { + const { diagnostics } = getDiagnostics('function foo() {}; html``'); + hasNoDiagnostics(t, diagnostics); +}); + +tsTest("Event binding: Called function is not bindable", t => { + const { diagnostics } = getDiagnostics('function foo() {}; html``'); + hasDiagnostic(t, diagnostics, "no-incompatible-type-binding"); +}); + +tsTest("Event binding: Any type is bindable", t => { + const { diagnostics } = getDiagnostics('html``'); + hasNoDiagnostics(t, diagnostics); +}); + +tsTest("Event binding: Object with callable 'handleEvent' is bindable 1", t => { + const { diagnostics } = getDiagnostics('html``'); + hasNoDiagnostics(t, diagnostics); +}); + +tsTest("Event binding: Object with callable 'handleEvent' is bindable 2", t => { + const { diagnostics } = getDiagnostics('function foo() {}; html``'); + hasNoDiagnostics(t, diagnostics); +}); + +tsTest("Event binding: Object with called 'handleEvent' is not bindable", t => { + const { diagnostics } = getDiagnostics('function foo() {}; html``'); + hasDiagnostic(t, diagnostics, "no-incompatible-type-binding"); +}); + +tsTest("Event binding: Object literal without 'handleEvent' is not bindable", t => { + const { diagnostics } = getDiagnostics('function foo() {}; html``'); + hasDiagnostic(t, diagnostics, "no-incompatible-type-binding"); +}); + +tsTest("Event binding: Mixed value binding with first expression being callable is bindable", t => { + const { diagnostics } = getDiagnostics('html``'); + hasNoDiagnostics(t, diagnostics); +}); diff --git a/packages/lit-analyzer/test/rules/no-noncallable-event-binding.ts b/packages/lit-analyzer/test/rules/no-noncallable-event-binding.ts deleted file mode 100644 index 79744cee..00000000 --- a/packages/lit-analyzer/test/rules/no-noncallable-event-binding.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { getDiagnostics } from "../helpers/analyze"; -import { hasDiagnostic, hasNoDiagnostics } from "../helpers/assert"; -import { tsTest } from "../helpers/ts-test"; - -tsTest("Event binding: Callable value is bindable", t => { - const { diagnostics } = getDiagnostics('html``'); - hasNoDiagnostics(t, diagnostics); -}); - -tsTest("Event binding: Non callback value is not bindable", t => { - const { diagnostics } = getDiagnostics('html``'); - hasDiagnostic(t, diagnostics, "no-noncallable-event-binding"); -}); - -tsTest("Event binding: Number is not bindable", t => { - const { diagnostics } = getDiagnostics('html``'); - hasDiagnostic(t, diagnostics, "no-noncallable-event-binding"); -}); - -tsTest("Event binding: Function is bindable", t => { - const { diagnostics } = getDiagnostics('function foo() {}; html``'); - hasNoDiagnostics(t, diagnostics); -}); - -tsTest("Event binding: Called function is not bindable", t => { - const { diagnostics } = getDiagnostics('function foo() {}; html``'); - hasDiagnostic(t, diagnostics, "no-noncallable-event-binding"); -}); - -tsTest("Event binding: Any type is bindable", t => { - const { diagnostics } = getDiagnostics('html``'); - hasNoDiagnostics(t, diagnostics); -}); - -tsTest("Event binding: Object with callable 'handleEvent' is bindable 1", t => { - const { diagnostics } = getDiagnostics('html``'); - hasNoDiagnostics(t, diagnostics); -}); - -tsTest("Event binding: Object with callable 'handleEvent' is bindable 2", t => { - const { diagnostics } = getDiagnostics('function foo() {}; html``'); - hasNoDiagnostics(t, diagnostics); -}); - -tsTest("Event binding: Object with called 'handleEvent' is not bindable", t => { - const { diagnostics } = getDiagnostics('function foo() {}; html``'); - hasDiagnostic(t, diagnostics, "no-noncallable-event-binding"); -}); - -tsTest("Event binding: Object literal without 'handleEvent' is not bindable", t => { - const { diagnostics } = getDiagnostics('function foo() {}; html``'); - hasDiagnostic(t, diagnostics, "no-noncallable-event-binding"); -}); - -tsTest("Event binding: Mixed value binding with first expression being callable is bindable", t => { - const { diagnostics } = getDiagnostics('html``'); - hasNoDiagnostics(t, diagnostics); -}); diff --git a/packages/ts-lit-plugin/README.md b/packages/ts-lit-plugin/README.md index a4905df5..ab09fe98 100644 --- a/packages/ts-lit-plugin/README.md +++ b/packages/ts-lit-plugin/README.md @@ -129,7 +129,6 @@ Each rule can have severity of `off`, `warning` or `error`. You can toggle rules | :------ | ----------- | --------------- | --------------- | | [no-invalid-boolean-binding](#-no-invalid-boolean-binding) | Disallow boolean attribute bindings on non-boolean types. | error | error | | [no-expressionless-property-binding](#-no-expressionless-property-binding) | Disallow property bindings without an expression. | error | error | -| [no-noncallable-event-binding](#-no-noncallable-event-binding) | Disallow event listener bindings with a noncallable type. | error | error | | [no-boolean-in-attribute-binding](#-no-boolean-in-attribute-binding) | Disallow attribute bindings with a boolean type. | error | error | | [no-complex-attribute-binding](#-no-complex-attribute-binding) | Disallow attribute bindings with a complex type. | error | error | | [no-nullable-attribute-binding](#-no-nullable-attribute-binding) | Disallow attribute bindings with nullable types such as "null" or "undefined". | error | error | @@ -394,26 +393,6 @@ The following example is not considered a warning: html`` ``` -#### 🌀 no-noncallable-event-binding - -It's a common mistake to incorrectly call the function when setting up an event handler binding instead of passing a reference to the function. This makes the function call whenever the code evaluates. - -The following examples are considered warnings: - - -```js -html`` -html`` -``` - -The following examples are not considered warnings: - - -```js -html`` -html`` -``` - #### 😈 no-boolean-in-attribute-binding You should not be binding to a boolean type using an attribute binding because it could result in binding the string "true" or "false". Instead you should be using a **boolean** attribute binding. @@ -484,6 +463,8 @@ html`` html`` html`` html`` +html`` +html`` ``` The following examples are not considered warnings: @@ -494,6 +475,8 @@ html`` html`` html`` html`` +html`` +html`` ``` #### 💥 no-invalid-directive-binding diff --git a/packages/vscode-lit-plugin/README.md b/packages/vscode-lit-plugin/README.md index 8dce4cf0..1c6d1925 100644 --- a/packages/vscode-lit-plugin/README.md +++ b/packages/vscode-lit-plugin/README.md @@ -65,7 +65,6 @@ Each rule can have severity of `off`, `warning` or `error`. You can toggle rules | :------ | ----------- | --------------- | --------------- | | [no-invalid-boolean-binding](#-no-invalid-boolean-binding) | Disallow boolean attribute bindings on non-boolean types. | error | error | | [no-expressionless-property-binding](#-no-expressionless-property-binding) | Disallow property bindings without an expression. | error | error | -| [no-noncallable-event-binding](#-no-noncallable-event-binding) | Disallow event listener bindings with a noncallable type. | error | error | | [no-boolean-in-attribute-binding](#-no-boolean-in-attribute-binding) | Disallow attribute bindings with a boolean type. | error | error | | [no-complex-attribute-binding](#-no-complex-attribute-binding) | Disallow attribute bindings with a complex type. | error | error | | [no-nullable-attribute-binding](#-no-nullable-attribute-binding) | Disallow attribute bindings with nullable types such as "null" or "undefined". | error | error | @@ -330,26 +329,6 @@ The following example is not considered a warning: html`` ``` -#### 🌀 no-noncallable-event-binding - -It's a common mistake to incorrectly call the function when setting up an event handler binding instead of passing a reference to the function. This makes the function call whenever the code evaluates. - -The following examples are considered warnings: - - -```js -html`` -html`` -``` - -The following examples are not considered warnings: - - -```js -html`` -html`` -``` - #### 😈 no-boolean-in-attribute-binding You should not be binding to a boolean type using an attribute binding because it could result in binding the string "true" or "false". Instead you should be using a **boolean** attribute binding. @@ -420,6 +399,8 @@ html`` html`` html`` html`` +html`` +html`` ``` The following examples are not considered warnings: @@ -430,6 +411,8 @@ html`` html`` html`` html`` +html`` +html`` ``` #### 💥 no-invalid-directive-binding diff --git a/packages/vscode-lit-plugin/package.json b/packages/vscode-lit-plugin/package.json index a1d5faa1..ce986f88 100644 --- a/packages/vscode-lit-plugin/package.json +++ b/packages/vscode-lit-plugin/package.json @@ -285,17 +285,6 @@ "error" ] }, - "lit-plugin.rules.no-noncallable-event-binding": { - "type": "string", - "description": "Disallow event listener bindings with a noncallable type.", - "default": "default", - "enum": [ - "default", - "off", - "warning", - "error" - ] - }, "lit-plugin.rules.no-boolean-in-attribute-binding": { "type": "string", "description": "Disallow attribute bindings with a boolean type.", diff --git a/packages/vscode-lit-plugin/schemas/tsconfig.schema.json b/packages/vscode-lit-plugin/schemas/tsconfig.schema.json index 74ed81cb..c0167537 100644 --- a/packages/vscode-lit-plugin/schemas/tsconfig.schema.json +++ b/packages/vscode-lit-plugin/schemas/tsconfig.schema.json @@ -148,11 +148,6 @@ "description": "Disallow property bindings without an expression.", "enum": ["off", "warning", "error"] }, - "no-noncallable-event-binding": { - "type": "string", - "description": "Disallow event listener bindings with a noncallable type.", - "enum": ["off", "warning", "error"] - }, "no-boolean-in-attribute-binding": { "type": "string", "description": "Disallow attribute bindings with a boolean type.",