Files
react-native-devtools-frontend/scripts/eslint_rules/lib/check_component_naming.js
T
Jack Franklin e9f62582dd Lint for consistent component tag names in component definitions
This CL lints that for a given component that all the references to
its tag name are the same.

```
class Foo extends HTMLElement {
  // Check that this name
  static litTagName = LitHtml.literal\`devtools-foo\`
}

// And this name
ComponentHelpers.CustomElements.defineComponent('devtools-foo', Foo);

declare global {
  interface HTMLElementTagNameMap {
    // And this one are the same
    'devtools-foo': Foo
  }
}
```

Bug: 1153077
Change-Id: I29694449cb37950d1a5ff5391779e16e462926e2
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2897279
Reviewed-by: Paul Lewis <aerotwist@chromium.org>
Commit-Queue: Jack Franklin <jacktfranklin@chromium.org>
2021-05-14 12:57:20 +00:00

146 lines
5.5 KiB
JavaScript

// Copyright 2021 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.
'use strict';
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'check license headers',
category: 'Possible Errors',
},
fixable: 'code',
schema: [],
messages: {
nonMatchingTagName:
'Found inconsistent tag names in the component definition. Make sure the static litTagName, the tag in the defineComponent() call and the TS interface key are all identical.',
noStaticTagName: 'Found a component class that does not define a static litTagName property.',
noTSInterface: 'Could not find the TS interface declaration for the component.',
noDefineCall: 'Could not find a defineComponent() call for the component.',
defineCallNonLiteral: 'defineComponent() first argument must be a string literal.',
staticLiteralInvalid: 'static litTagName must use a literal string, with no interpolation.'
}
},
create: function(context) {
function findComponentClassDefinition(programNode) {
return programNode.body.find(node => {
return node.type === 'ClassDeclaration' && node.superClass && node.superClass.name === 'HTMLElement';
});
}
function findComponentTagNameFromStaticProperty(classBodyNode) {
return classBodyNode.body.find(node => {
return node.type === 'ClassProperty' && node.key.name === 'litTagName';
});
}
function findCustomElementsDefineComponentCall(programNode) {
return programNode.body.find(node => {
if (node.type !== 'ExpressionStatement') {
return false;
}
if (node.expression.callee.property) {
// matches ComponentHelpers.CustomElements.defineComponent()
return node.expression.callee.property.name === 'defineComponent';
}
// matches defineComponent() which may have been destructured
return node.expression.callee.name === 'defineComponent';
});
}
function findTypeScriptDeclareGlobalHTMLInterfaceCall(programNode) {
const matchingNode = programNode.body.find(node => {
// matches `declare global {}`
const isGlobalCall = node.type === 'TSModuleDeclaration' && node.id.name === 'global';
if (!isGlobalCall) {
return false;
}
return node.body.body.find(node => {
return node.type === 'TSInterfaceDeclaration' && node.id.name === 'HTMLElementTagNameMap';
});
});
if (!matchingNode) {
return undefined;
}
return matchingNode.body.body.find(node => {
return node.type === 'TSInterfaceDeclaration' && node.id.name === 'HTMLElementTagNameMap';
});
}
return {
Program(node) {
const componentClassDefinition = findComponentClassDefinition(node);
if (!componentClassDefinition) {
return;
}
const componentTagNameNode = findComponentTagNameFromStaticProperty(componentClassDefinition.body);
if (!componentTagNameNode) {
context.report({node: componentClassDefinition, messageId: 'noStaticTagName'});
return;
}
if (componentTagNameNode.value.quasi.quasis.length > 1) {
// This means that there's >1 template parts, which means they are
// being split by an interpolated value, which is not allowed.
context.report({node: componentTagNameNode, messageId: 'staticLiteralInvalid'});
return;
}
// Grab the name of the component, e.g:
// LitHtml.literal`devtools-foo` will pull "devtools-foo" out.
const componentTagName = componentTagNameNode.value.quasi.quasis[0].value.cooked;
// Now find the CustomElements.defineComponent() line
const customElementsDefineCall = findCustomElementsDefineComponentCall(node);
if (!customElementsDefineCall) {
context.report({node, messageId: 'noDefineCall'});
return;
}
const firstArgumentToDefineCall = customElementsDefineCall.expression.arguments[0];
if (!firstArgumentToDefineCall || firstArgumentToDefineCall.type !== 'Literal') {
context.report({
node: customElementsDefineCall,
messageId: 'defineCallNonLiteral',
});
return;
}
const tagNamePassedToDefineCall = firstArgumentToDefineCall.value;
if (tagNamePassedToDefineCall !== componentTagName) {
context.report({node: customElementsDefineCall, messageId: 'nonMatchingTagName'});
return;
}
const typeScriptInterfaceDeclare = findTypeScriptDeclareGlobalHTMLInterfaceCall(node);
if (!typeScriptInterfaceDeclare) {
context.report({
node,
messageId: 'noTSInterface',
});
return;
}
const keyForComponent = typeScriptInterfaceDeclare.body.body.find(node => {
// Find the declaration line for the component tag name.
return node.type === 'TSPropertySignature' && node.key.value === componentTagName;
});
// If we didn't find it, that means it's either not there or it's there
// but doesn't have the right name, so error.
if (!keyForComponent) {
context.report({node: typeScriptInterfaceDeclare, messageId: 'nonMatchingTagName'});
return;
}
// if we got here, everything is valid and the name is correct in all three locations
}
};
}
};