UNPKG

chrome-devtools-frontend

Version:
144 lines (128 loc) 5.74 kB
// Copyright 2024 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Disallow customized built-in elements. * * Cusomized built-in elements are not supported in Safari and will likely never * be supported in Safari ever, which has already caused problems for example * for http://trace.cafe (https://crbug.com/379694205). * * Customized built-in elemens are also incompatible with the Vision for the * Chrome DevTools UI Engineering. * * @see https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/is * @see http://go/chrome-devtools:ui-engineering-proposal */ import type {TSESTree} from '@typescript-eslint/utils'; import {createRule} from './utils/ruleCreator.ts'; // Define types based on TSESTree type Node = TSESTree.Node; type MemberExpression = TSESTree.MemberExpression; type Identifier = TSESTree.Identifier; type ObjectExpression = TSESTree.ObjectExpression; // Define MessageIds used in the rule type MessageIds =|'unexpectedCustomElementsDefineWithExtends'|'unexpectedExtendsBuiltinElement'; const BUILTIN_ELEMENT_REGEXP = /^HTML\w+Element$/; const GLOBAL_THIS_NAMES = new Set(['globalThis', 'self', 'window']); // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ export default createRule<[], MessageIds>({ // Add type parameters for options and messageIds name: 'no-customized-builtin-elements', // Rule name should match the file name meta: { type: 'suggestion', docs: { description: 'Disallow customized built-in elements', category: 'Best Practices', }, messages: { // Define messages corresponding to MessageIds unexpectedCustomElementsDefineWithExtends: 'Unexpected call to `{{ callee }}` extending a built-in element. Customized built-in elements are disallowed, use an autonomous custom element instead', unexpectedExtendsBuiltinElement: 'Unexpected built-in element `{{ superClass }}` subclass. Customized built-in elements are disallowed, use an autonomous custom element instead', }, schema: [], // no options }, defaultOptions: [], // Add defaultOptions create: function(context) { // Type the node parameter function isCustomElementsDefine(calleeNode: Node): calleeNode is MemberExpression { if (calleeNode.type !== 'MemberExpression' || calleeNode.property.type !== 'Identifier' || calleeNode.property.name !== 'define') { return false; } // Test for the common case `customElements.define(...)` if (calleeNode.object.type === 'Identifier' && calleeNode.object.name === 'customElements') { return true; } // Test for `globalThis.customElements.define(...)` return ( calleeNode.object.type === 'MemberExpression' && calleeNode.object.property.type === 'Identifier' && calleeNode.object.property.name === 'customElements' && calleeNode.object.object.type === 'Identifier' && GLOBAL_THIS_NAMES.has(calleeNode.object.object.name)); } // Type the node parameter function isObjectLiteralWithProperty(node: Node, propertyName: string): node is ObjectExpression { if (node.type !== 'ObjectExpression') { return false; } for (const property of node.properties) { // Ensure property is of type Property before accessing key if (property.type === 'Property') { if (property.key.type === 'Identifier' && property.key.name === propertyName) { return true; } if (property.key.type === 'Literal' && property.key.value === propertyName) { return true; } } } return false; } // Type the node parameter function isBuiltinElementClass(superNode: Node): superNode is Identifier|MemberExpression { // Test for the common case `HTMLFooElement`. if (superNode.type === 'Identifier' && BUILTIN_ELEMENT_REGEXP.test(superNode.name)) { return true; } // Test for `globalThis.HTMLFooElement` return ( superNode.type === 'MemberExpression' && superNode.object.type === 'Identifier' && GLOBAL_THIS_NAMES.has(superNode.object.name) && superNode.property.type === 'Identifier' && BUILTIN_ELEMENT_REGEXP.test(superNode.property.name)); } function reportError(node: Node, messageId: MessageIds, data?: Record<string, string>): void { context.report({ node, messageId, data, }); } const sourceCode = context.sourceCode; return { // Type the node parameter CallExpression(node) { if (isCustomElementsDefine(node.callee) && node.arguments.length >= 3 && // Ensure argument exists and is a Node before passing node.arguments[2] && isObjectLiteralWithProperty(node.arguments[2], 'extends')) { // sourceCode is already defined above const callee = sourceCode.getText(node.callee); reportError(node, 'unexpectedCustomElementsDefineWithExtends', { callee, }); } }, ClassDeclaration(node) { if (node.superClass && isBuiltinElementClass(node.superClass)) { // sourceCode is already defined above const superClass = sourceCode.getText(node.superClass); reportError(node, 'unexpectedExtendsBuiltinElement', {superClass}); } }, }; }, });