From f815257a89a32fb3b62282601237cc3d3b263840 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 11 Aug 2019 10:21:13 +1200 Subject: [PATCH] chore(tsutils): refactor `StringNode` & related functions --- src/rules/no-empty-title.ts | 26 ++----- src/rules/no-identical-title.ts | 1 + src/rules/tsUtils.ts | 116 +++++++++++++++++++++++++------- 3 files changed, 101 insertions(+), 42 deletions(-) diff --git a/src/rules/no-empty-title.ts b/src/rules/no-empty-title.ts index c75302b3d..e18ab07a2 100644 --- a/src/rules/no-empty-title.ts +++ b/src/rules/no-empty-title.ts @@ -1,12 +1,4 @@ -import { - createRule, - getStringValue, - hasExpressions, - isDescribe, - isStringNode, - isTemplateLiteral, - isTestCase, -} from './tsUtils'; +import { createRule, isDescribe, isStringNode, isTestCase } from './tsUtils'; export default createRule({ name: __filename, @@ -31,18 +23,14 @@ export default createRule({ return; } const [firstArgument] = node.arguments; - if (!isStringNode(firstArgument)) { + if (!firstArgument || !isStringNode(firstArgument, '')) { return; } - if (isTemplateLiteral(firstArgument) && hasExpressions(firstArgument)) { - return; - } - if (getStringValue(firstArgument) === '') { - context.report({ - messageId: isDescribe(node) ? 'describe' : 'test', - node, - }); - } + + context.report({ + messageId: isDescribe(node) ? 'describe' : 'test', + node, + }); }, }; }, diff --git a/src/rules/no-identical-title.ts b/src/rules/no-identical-title.ts index 2db27a78b..80bf490c0 100644 --- a/src/rules/no-identical-title.ts +++ b/src/rules/no-identical-title.ts @@ -46,6 +46,7 @@ export default createRule({ } const [firstArgument] = node.arguments; if ( + !firstArgument || !isStringNode(firstArgument) || (isTemplateLiteral(firstArgument) && hasExpressions(firstArgument)) ) { diff --git a/src/rules/tsUtils.ts b/src/rules/tsUtils.ts index f8fabb850..b9f98b723 100644 --- a/src/rules/tsUtils.ts +++ b/src/rules/tsUtils.ts @@ -15,6 +15,99 @@ export const createRule = ESLintUtils.RuleCreator(name => { return `${REPO_URL}/blob/v${version}/docs/rules/${ruleName}.md`; }); +/** + * A `Literal` with a `value` of type `string`. + */ +export interface StringLiteral + extends TSESTree.Literal { + value: Value; +} + +/** + * Checks if the given `node` is a `StringLiteral`. + * + * If a `value` is provided & the `node` is a `StringLiteral`, + * the `value` will be compared to that of the `StringLiteral`. + * + * @param {Node} node + * @param {V?} value + * + * @return {node is StringLiteral} + * + * @template {V}. + */ +const isStringLiteral = ( + node: TSESTree.Node, + value?: V, +): node is StringLiteral => + node.type === AST_NODE_TYPES.Literal && + typeof node.value === 'string' && + (value === undefined || node.value === value); + +interface TemplateLiteral + extends TSESTree.TemplateLiteral { + quasis: [TSESTree.TemplateElement & { value: { raw: Value; cooked: Value } }]; +} + +/** + * Checks if the given `node` is a `TemplateLiteral`. + * + * Complex `TemplateLiteral`s are not considered specific, and so will return `false`. + * + * If a `value` is provided & the `node` is a `TemplateLiteral`, + * the `value` will be compared to that of the `TemplateLiteral`. + * + * @param {Node} node + * @param {V?} value + * + * @return {node is TemplateLiteral} + * + * @template V + */ +export const isTemplateLiteral = ( + node: TSESTree.Node, + value?: V, +): node is TemplateLiteral => + node.type === AST_NODE_TYPES.TemplateLiteral && + (value === undefined || + (node.quasis.length === 1 && // bail out if not simple + node.quasis[0].value.raw === value)); + +type StringNode = + | StringLiteral + | TemplateLiteral; + +/** + * Checks if the given `node` is a {@link StringNode}. + * + * @param {Node} node + * @param {V?} specifics + * + * @return {node is StringNode} + * + * @template V + */ +export const isStringNode = ( + node: TSESTree.Node, + specifics?: V, +): node is StringNode => + isStringLiteral(node, specifics) || isTemplateLiteral(node, specifics); + +/** + * Gets the value of the given `StringNode`. + * + * If the `node` is a `TemplateLiteral`, the `raw` value is used; + * otherwise, `value` is returned instead. + * + * @param {StringNode} node + * + * @return {S} + * + * @template S + */ +export const getStringValue = (node: StringNode): S => + isTemplateLiteral(node) ? node.quasis[0].value.raw : node.value; + interface JestExpectIdentifier extends TSESTree.Identifier { name: 'expect'; } @@ -217,34 +310,11 @@ export const isLiteralNode = (node: { type: AST_NODE_TYPES; }): node is TSESTree.Literal => node.type === AST_NODE_TYPES.Literal; -export interface StringLiteral extends TSESTree.Literal { - value: string; -} - -export type StringNode = StringLiteral | TSESTree.TemplateLiteral; - -export const isStringLiteral = (node: TSESTree.Node): node is StringLiteral => - node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string'; - -export const isTemplateLiteral = ( - node: TSESTree.Node, -): node is TSESTree.TemplateLiteral => - node && node.type === AST_NODE_TYPES.TemplateLiteral; - -export const isStringNode = ( - node: TSESTree.Node | undefined, -): node is StringNode => - node !== undefined && (isStringLiteral(node) || isTemplateLiteral(node)); - export const hasExpressions = ( node: TSESTree.Node, ): node is TSESTree.Expression => 'expressions' in node && node.expressions.length > 0; -/* istanbul ignore next we'll need this later */ -export const getStringValue = (arg: StringNode): string => - isTemplateLiteral(arg) ? arg.quasis[0].value.raw : arg.value; - const collectReferences = (scope: TSESLint.Scope.Scope) => { const locals = new Set(); const unresolved = new Set();