-
Notifications
You must be signed in to change notification settings - Fork 498
/
Copy pathno-customized-builtin-elements.ts
143 lines (128 loc) · 5.61 KB
/
no-customized-builtin-elements.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// 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?: {[key: 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});
}
},
};
},
});