chrome-devtools-frontend
Version:
Chrome DevTools UI
159 lines (139 loc) • 5.73 kB
text/typescript
// Copyright 2020 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.
import type {TSESTree} from '@typescript-eslint/utils';
import {createRule} from './utils/ruleCreator.ts';
export default createRule({
name: 'lit-template-result-or-nothing',
meta: {
type: 'problem',
docs: {
description: 'Enforce use of Lit.LitTemplate type rather than union with Lit.nothing or {}',
category: 'Possible Errors',
},
fixable: 'code',
messages: {
useLitTemplateOverEmptyObject: 'Prefer Lit.LitTemplate type over a union with {}',
useLitTemplateOverTypeOfNothing: 'Prefer Lit.LitTemplate type over a union with typeof Lit.nothing',
},
schema: [], // no options
},
defaultOptions: [],
create: function(context) {
const sourceCode = context.sourceCode;
const UNION_TYPE_FOR_LIT_TEMPLATE = 'Lit.LitTemplate';
function checkUnionReturnTypeForLit(node: TSESTree.TSUnionType): void {
// We want to go through the types in the union and match if:
// 1. We find `Lit.TemplateResult` and `{}`
// 2. We find `Lit.TemplateResult` and `typeof Lit.nothing`.
// Otherwise, this node is OK.
let templateResultNode: TSESTree.TSTypeReference|null = null;
let literalEmptyObjectNode: TSESTree.TSTypeLiteral|null = null;
let litNothingNode: TSESTree.TSTypeQuery|null = null;
const nonLitRelatedNodesInUnion = new Set<TSESTree.TypeNode>();
for (const typeNode of node.types) {
// This matches a type reference of X.y. Now we see if X === 'Lit' and y === 'TemplateResult'
if (typeNode.type === 'TSTypeReference' && typeNode.typeName.type === 'TSQualifiedName' &&
typeNode.typeName.left.type === 'Identifier') {
const leftText = typeNode.typeName.left.name;
const rightText = typeNode.typeName.right.name;
if (leftText === 'Lit' && rightText === 'TemplateResult') {
templateResultNode = typeNode;
continue;
}
} else if (typeNode.type === 'TSTypeLiteral') {
// The TSTypeLiteral type matches against the literal `{}` type.
literalEmptyObjectNode = typeNode;
continue;
} else if (
typeNode.type === 'TSTypeQuery' && typeNode.exprName.type === 'TSQualifiedName' &&
typeNode.exprName.left.type === 'Identifier') {
// matches `typeof X.y`
const leftText = typeNode.exprName.left.name;
const rightText = typeNode.exprName.right.name;
if (leftText === 'Lit' && rightText === 'nothing') {
litNothingNode = typeNode;
continue;
}
}
nonLitRelatedNodesInUnion.add(typeNode);
}
// We didn't find Lit.TemplateResult, so bail.
if (!templateResultNode) {
return;
}
if (!litNothingNode && !literalEmptyObjectNode) {
// We found TemplateResult with no `typeof Lit.nothing` or `{}`, so
// bail.
return;
}
// If we found a union type of:
// Lit.TemplateResult|{}|number
// That needs to become:
// Lit.LitTemplate|number
// So we capture all the non-lit related types in the union, and get
// their text content, so we can keep them around when we run the fixer.
const nonLitRelatedTypesToKeep = Array.from(
nonLitRelatedNodesInUnion,
node => {
return sourceCode.getText(node);
},
);
const newText = [
UNION_TYPE_FOR_LIT_TEMPLATE,
...nonLitRelatedTypesToKeep,
].join('|');
context.report({
node,
messageId: litNothingNode ? 'useLitTemplateOverTypeOfNothing' : 'useLitTemplateOverEmptyObject',
fix(fixer) {
return fixer.replaceText(node, newText);
},
});
}
function checkTSTypeAnnotationForPotentialIssue(node: TSESTree.TSTypeAnnotation): void {
const annotation = node.typeAnnotation;
if (annotation.type === 'TSUnionType') {
// matches foo(): X|Y
checkUnionReturnTypeForLit(annotation);
} else if (annotation.type === 'TSTypeReference') {
// matches many things, including foo(): Promise<X|Y>, which we do want
// to check.
if (annotation.typeName.type !== 'Identifier' || annotation.typeName.name !== 'Promise') {
// If it's not a promise, bail out.
return;
}
// Represents the generic type passed to the promise: if our code is
// Promise<X>, this node represents the X.
const promiseGenericNode = annotation.typeArguments?.params[0];
if (promiseGenericNode && promiseGenericNode.type === 'TSUnionType') {
checkUnionReturnTypeForLit(promiseGenericNode);
}
}
}
function checkFunctionDeclarationOrExpressionForUnionType(
node: TSESTree.FunctionDeclaration|TSESTree.FunctionExpression|TSESTree.ArrowFunctionExpression): void {
if (!node.returnType) {
return;
}
checkTSTypeAnnotationForPotentialIssue(node.returnType);
}
return {
// function() {}
FunctionDeclaration(node) {
checkFunctionDeclarationOrExpressionForUnionType(node);
},
// Match functions defined inside classes
FunctionExpression(node) {
checkFunctionDeclarationOrExpressionForUnionType(node);
},
// Match values in interfaces or types.
TSPropertySignature(node) {
if (!node.typeAnnotation) {
return;
}
checkTSTypeAnnotationForPotentialIssue(node.typeAnnotation);
},
};
},
});