UNPKG

lit-analyzer

Version:

CLI that type checks bindings in lit-html templates

238 lines (236 loc) 11.7 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.isAssignableInPrimitiveArray = exports.isAssignableToTypeWithStringCoercion = exports.isAssignableInAttributeBinding = void 0; var ts_simple_type_1 = require("ts-simple-type"); var html_node_attr_assignment_types_js_1 = require("../../../analyze/types/html-node/html-node-attr-assignment-types.js"); var range_util_js_1 = require("../../../analyze/util/range-util.js"); var type_util_js_1 = require("../../../analyze/util/type-util.js"); var is_lit_directive_js_1 = require("../directive/is-lit-directive.js"); var is_assignable_binding_under_security_system_js_1 = require("./is-assignable-binding-under-security-system.js"); var is_assignable_to_type_js_1 = require("./is-assignable-to-type.js"); function isAssignableInAttributeBinding(htmlAttr, _a, context) { var typeA = _a.typeA, typeB = _a.typeB; var assignment = htmlAttr.assignment; if (assignment == null) return undefined; if (assignment.kind === html_node_attr_assignment_types_js_1.HtmlNodeAttrAssignmentKind.BOOLEAN) { if (!(0, is_assignable_to_type_js_1.isAssignableToType)({ typeA: typeA, typeB: typeB }, context)) { context.report({ location: (0, range_util_js_1.rangeFromHtmlNodeAttr)(htmlAttr), message: "Type '".concat((0, ts_simple_type_1.typeToString)(typeB), "' is not assignable to '").concat((0, ts_simple_type_1.typeToString)(typeA), "'") }); return false; } } else { if (assignment.kind !== html_node_attr_assignment_types_js_1.HtmlNodeAttrAssignmentKind.STRING) { // Purely static attributes are never security checked, they're handled // in the lit-html internals as trusted by default, because they can // not contain untrusted data, they were written by the developer. // // For everything else, we may need to apply a different type comparison // for some security-sensitive built in attributes and properties (like // <script src>). var securitySystemResult = (0, is_assignable_binding_under_security_system_js_1.isAssignableBindingUnderSecuritySystem)(htmlAttr, { typeA: typeA, typeB: typeB }, context); if (securitySystemResult !== undefined) { // The security diagnostics take precedence here, // and we should not do any more checking. return securitySystemResult; } } var primitiveArrayTypeResult = isAssignableInPrimitiveArray(assignment, { typeA: typeA, typeB: typeB }, context); if (primitiveArrayTypeResult !== undefined) { return primitiveArrayTypeResult; } if (!(0, is_assignable_to_type_js_1.isAssignableToType)({ typeA: typeA, typeB: typeB }, context, { isAssignable: isAssignableToTypeWithStringCoercion })) { context.report({ location: (0, range_util_js_1.rangeFromHtmlNodeAttr)(htmlAttr), message: "Type '".concat((0, ts_simple_type_1.typeToString)(typeB), "' is not assignable to '").concat((0, ts_simple_type_1.typeToString)(typeA), "'") }); return false; } } return true; } exports.isAssignableInAttributeBinding = isAssignableInAttributeBinding; /** * Assignability check that simulates string coercion * This is used to type check attribute bindings * @param typeA * @param typeB * @param options */ function isAssignableToTypeWithStringCoercion(typeA, typeB, options) { var safeOptions = __assign(__assign({}, options), { isAssignable: undefined }); switch (typeB.kind) { /*case "NULL": return _isAssignableToType(typeA, { kind: "STRING_LITERAL", value: "null" }, safeOptions); case "UNDEFINED": return _isAssignableToType(typeA, { kind: "STRING_LITERAL", value: "undefined" }, safeOptions); */ case "ALIAS": case "FUNCTION": case "GENERIC_ARGUMENTS": // Always return true if this is a lit directive if ((0, is_lit_directive_js_1.isLitDirective)(typeB)) { return true; } break; case "OBJECT": case "CLASS": case "INTERFACE": // This allows for types like: string | (part: Part) => void return (0, ts_simple_type_1.isAssignableToType)(typeA, { kind: "STRING_LITERAL", value: "[object Object]" }, safeOptions); case "STRING_LITERAL": /*if (typeA.kind === "ARRAY" && typeA.type.kind === "STRING_LITERAL") { }*/ // Take into account that the empty string is is equal to true if (typeB.value.length === 0) { if ((0, ts_simple_type_1.isAssignableToType)(typeA, { kind: "BOOLEAN_LITERAL", value: true }, safeOptions)) { return true; } } // Test if a potential string literal is a assignable to a number // Example: max="123" if (!isNaN(typeB.value)) { if ((0, ts_simple_type_1.isAssignableToType)(typeA, { kind: "NUMBER_LITERAL", value: Number(typeB.value) }, safeOptions)) { return true; } } break; case "BOOLEAN": // Test if a boolean coerced string is possible. // Example: aria-expanded="${this.open}" return (0, ts_simple_type_1.isAssignableToType)(typeA, { kind: "UNION", types: [ { kind: "STRING_LITERAL", value: "true" }, { kind: "STRING_LITERAL", value: "false" } ] }, safeOptions); case "BOOLEAN_LITERAL": /** * Test if a boolean literal coerced to string is possible * Example: aria-expanded="${this.open}" */ return (0, ts_simple_type_1.isAssignableToType)(typeA, { kind: "STRING_LITERAL", value: String(typeB.value) }, safeOptions); case "NUMBER": // Test if a number coerced to string is possible // Example: value="${this.max}" if ((0, ts_simple_type_1.isAssignableToType)(typeA, { kind: "STRING" }, safeOptions)) { return true; } break; case "NUMBER_LITERAL": // Test if a number literal coerced to string is possible // Example: value="${this.max}" if ((0, ts_simple_type_1.isAssignableToType)(typeA, { kind: "STRING_LITERAL", value: String(typeB.value) }, safeOptions)) { return true; } break; } return undefined; } exports.isAssignableToTypeWithStringCoercion = isAssignableToTypeWithStringCoercion; /** * Certain attributes like "role" are string literals, but should be type checked * by comparing each item in the white-space-separated array against typeA * @param assignment * @param typeA * @param typeB * @param context */ function isAssignableInPrimitiveArray(assignment, _a, context) { var e_1, _b; var typeA = _a.typeA, typeB = _a.typeB; // Only check "STRING" and "EXPRESSION" for now if (assignment.kind !== html_node_attr_assignment_types_js_1.HtmlNodeAttrAssignmentKind.STRING && assignment.kind !== html_node_attr_assignment_types_js_1.HtmlNodeAttrAssignmentKind.EXPRESSION) { return undefined; } // Check if typeA is marked as a "primitive array type" if ((0, type_util_js_1.isPrimitiveArrayType)(typeA) && typeB.kind === "STRING_LITERAL") { // Split a value like: "button listitem" into ["button", " ", "listitem"] var valuesAndWhitespace = typeB.value.split(/(\s+)/g); var valuesNotAssignable = []; var startOffset = assignment.location.start; var offset = 0; try { for (var valuesAndWhitespace_1 = __values(valuesAndWhitespace), valuesAndWhitespace_1_1 = valuesAndWhitespace_1.next(); !valuesAndWhitespace_1_1.done; valuesAndWhitespace_1_1 = valuesAndWhitespace_1.next()) { var value = valuesAndWhitespace_1_1.value; // Check all non-whitespace values if (value.match(/\s+/) == null && value !== "") { // Make sure that the the value is assignable to the union if (!(0, is_assignable_to_type_js_1.isAssignableToType)({ typeA: typeA, typeB: { kind: "STRING_LITERAL", value: value } }, context, { isAssignable: isAssignableToTypeWithStringCoercion })) { valuesNotAssignable.push(value); // If the assignment kind is "STRING" we can report diagnostics directly on the value in the HTML if (assignment.kind === "STRING") { context.report({ location: (0, range_util_js_1.documentRangeToSFRange)(assignment.htmlAttr.document, { start: startOffset + offset, end: startOffset + offset + value.length }), message: "The value '".concat(value, "' is not assignable to '").concat((0, ts_simple_type_1.typeToString)(typeA), "'") }); } } } offset += value.length; } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (valuesAndWhitespace_1_1 && !valuesAndWhitespace_1_1.done && (_b = valuesAndWhitespace_1.return)) _b.call(valuesAndWhitespace_1); } finally { if (e_1) throw e_1.error; } } // If the assignment kind as "EXPRESSION" report a single diagnostic on the attribute name if (assignment.kind === "EXPRESSION" && valuesNotAssignable.length > 0) { var multiple = valuesNotAssignable.length > 1; context.report({ location: (0, range_util_js_1.rangeFromHtmlNodeAttr)(assignment.htmlAttr), message: "The value".concat(multiple ? "s" : "", " ").concat(valuesNotAssignable.map(function (v) { return "'".concat(v, "'"); }).join(", "), " ").concat(multiple ? "are" : "is", " not assignable to '").concat((0, ts_simple_type_1.typeToString)(typeA), "'") }); } return valuesNotAssignable.length === 0; } return undefined; } exports.isAssignableInPrimitiveArray = isAssignableInPrimitiveArray;