Skip to content

Commit

Permalink
refactor: move guards to a module
Browse files Browse the repository at this point in the history
  • Loading branch information
JamieMason committed Jan 9, 2025
1 parent 949b6e3 commit 32c9fa7
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 211 deletions.
33 changes: 33 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { TSESTree } from '@typescript-eslint/types';

export type AnyFunctionBody = TSESTree.BlockStatement | TSESTree.Expression;
export type AnyFunction = TSESTree.FunctionDeclaration | TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression;
export type NameableFunction = TSESTree.FunctionDeclaration | TSESTree.FunctionExpression;
export type NamedFunction = NameableFunction & { id: TSESTree.Identifier };
export type GeneratorFunction = NameableFunction & { generator: true };
export type WithTypeParameters<T extends AnyFunction> = T & { typeParameters: TSESTree.TSTypeParameterDeclaration };
export type MessageId = keyof typeof MESSAGES_BY_ID;
export type Options = [ActualOptions];

export interface ActualOptions {
allowNamedFunctions: boolean;
classPropertiesAllowed: boolean;
disallowPrototype: boolean;
returnStyle: 'explicit' | 'implicit' | 'unchanged';
singleReturnOnly: boolean;
}

export const DEFAULT_OPTIONS: ActualOptions = {
allowNamedFunctions: false,
classPropertiesAllowed: false,
disallowPrototype: false,
returnStyle: 'unchanged',
singleReturnOnly: false,
};

export const MESSAGES_BY_ID = {
USE_ARROW_WHEN_FUNCTION: 'Prefer using arrow functions over plain functions',
USE_ARROW_WHEN_SINGLE_RETURN: 'Prefer using arrow functions when the function contains only a return',
USE_EXPLICIT: 'Prefer using explicit returns when the arrow function contain only a return',
USE_IMPLICIT: 'Prefer using implicit returns when the arrow function contain only a return',
} as const;
182 changes: 182 additions & 0 deletions src/guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils';
import { SourceCode } from '@typescript-eslint/utils/ts-eslint';
import {
ActualOptions,
AnyFunction,
AnyFunctionBody,
GeneratorFunction,
NamedFunction,
WithTypeParameters,
} from './config';

export const isAnyFunction = (value: TSESTree.Node): value is AnyFunction => {
return [
AST_NODE_TYPES.FunctionDeclaration,
AST_NODE_TYPES.FunctionExpression,
AST_NODE_TYPES.ArrowFunctionExpression,
].includes(value.type);
};

export const isReturnStatement = (value: unknown): value is TSESTree.ReturnStatement => {
return (value as TSESTree.Node)?.type === AST_NODE_TYPES.ReturnStatement;
};

export const isBlockStatementWithSingleReturn = (
body: AnyFunctionBody,
): body is TSESTree.BlockStatement & {
body: [TSESTree.ReturnStatement & { argument: TSESTree.Expression }];
} => {
return (
body.type === AST_NODE_TYPES.BlockStatement &&
body.body.length === 1 &&
isReturnStatement(body.body[0]) &&
body.body[0].argument !== null
);
};

export const hasImplicitReturn = (
body: AnyFunctionBody,
): body is Exclude<AnyFunctionBody, AST_NODE_TYPES.BlockStatement> => {
return body.type !== AST_NODE_TYPES.BlockStatement;
};

export const returnsImmediately = (fn: AnyFunction): boolean => {
return isBlockStatementWithSingleReturn(fn.body) || hasImplicitReturn(fn.body);
};

export const isExportedAsNamedExport = (node: AnyFunction): boolean =>
node.parent.type === AST_NODE_TYPES.ExportNamedDeclaration;

const getPreviousNode = (sourceCode: SourceCode, fn: AnyFunction): TSESTree.Node | null => {
const node = isExportedAsNamedExport(fn) ? fn.parent : fn;
const tokenBefore = sourceCode.getTokenBefore(node);
if (!tokenBefore) return null;
return sourceCode.getNodeByRangeIndex(tokenBefore.range[0]);
};

export const isOverloadedFunction = (sourceCode: SourceCode, fn: AnyFunction): boolean => {
const previousNode = getPreviousNode(sourceCode, fn);
return (
previousNode?.type === AST_NODE_TYPES.TSDeclareFunction ||
(previousNode?.type === AST_NODE_TYPES.ExportNamedDeclaration &&
previousNode.declaration?.type === AST_NODE_TYPES.TSDeclareFunction)
);
};

export const hasTypeParameters = <T extends AnyFunction>(fn: T): fn is WithTypeParameters<T> => {
return Boolean(fn.typeParameters);
};

export const isAsyncFunction = (node: AnyFunction): boolean => node.async === true;

export const isGeneratorFunction = (fn: AnyFunction): fn is GeneratorFunction => {
return fn.generator === true;
};

export const isAssertionFunction = <T extends AnyFunction>(
fn: T,
): fn is T & { returnType: TSESTree.TSTypeAnnotation } => {
return fn.returnType?.typeAnnotation.type === AST_NODE_TYPES.TSTypePredicate && fn.returnType?.typeAnnotation.asserts;
};

export const containsToken = (sourceCode: SourceCode, type: string, value: string, node: TSESTree.Node): boolean => {
return sourceCode.getTokens(node).some((token) => token.type === type && token.value === value);
};

export const containsSuper = (sourceCode: SourceCode, node: TSESTree.Node): boolean => {
return containsToken(sourceCode, 'Keyword', 'super', node);
};

export const containsThis = (sourceCode: SourceCode, node: TSESTree.Node): boolean => {
return containsToken(sourceCode, 'Keyword', 'this', node);
};

export const containsArguments = (sourceCode: SourceCode, node: TSESTree.Node): boolean => {
return containsToken(sourceCode, 'Identifier', 'arguments', node);
};

export const containsTokenSequence = (
sourceCode: SourceCode,
sequence: [string, string][],
node: TSESTree.Node,
): boolean => {
return sourceCode.getTokens(node).some((_, tokenIndex, tokens) => {
return sequence.every(([expectedType, expectedValue], i) => {
const actual = tokens[tokenIndex + i];
return actual && actual.type === expectedType && actual.value === expectedValue;
});
});
};

export const containsNewDotTarget = (sourceCode: SourceCode, node: TSESTree.Node): boolean => {
return containsTokenSequence(
sourceCode,
[
['Keyword', 'new'],
['Punctuator', '.'],
['Identifier', 'target'],
],
node,
);
};

export const isPrototypeAssignment = (sourceCode: SourceCode, node: AnyFunction): boolean => {
return sourceCode
.getAncestors(node)
.reverse()
.some((ancestor) => {
const isPropertyOfReplacementPrototypeObject =
ancestor.type === AST_NODE_TYPES.AssignmentExpression &&
ancestor.left &&
'property' in ancestor.left &&
ancestor.left.property &&
'name' in ancestor.left.property &&
ancestor.left.property.name === 'prototype';
const isMutationOfExistingPrototypeObject =
ancestor.type === AST_NODE_TYPES.AssignmentExpression &&
ancestor.left &&
'object' in ancestor.left &&
ancestor.left.object &&
'property' in ancestor.left.object &&
ancestor.left.object.property &&
'name' in ancestor.left.object.property &&
ancestor.left.object.property.name === 'prototype';
return isPropertyOfReplacementPrototypeObject || isMutationOfExistingPrototypeObject;
});
};

export const isWithinClassBody = (sourceCode: SourceCode, node: TSESTree.Node): boolean => {
return sourceCode
.getAncestors(node)
.reverse()
.some((ancestor) => {
return ancestor.type === AST_NODE_TYPES.ClassBody;
});
};

export const isNamedFunction = (fn: AnyFunction): fn is NamedFunction => fn.id !== null && fn.id.name !== null;

export const hasNameAndIsExportedAsDefaultExport = (fn: AnyFunction): fn is NamedFunction =>
isNamedFunction(fn) && fn.parent.type === AST_NODE_TYPES.ExportDefaultDeclaration;

export const isSafeTransformation = (
options: ActualOptions,
sourceCode: SourceCode,
fn: TSESTree.Node,
): fn is AnyFunction => {
const isSafe =
isAnyFunction(fn) &&
!isGeneratorFunction(fn) &&
!isAssertionFunction(fn) &&
!isOverloadedFunction(sourceCode, fn) &&
!containsThis(sourceCode, fn) &&
!containsSuper(sourceCode, fn) &&
!containsArguments(sourceCode, fn) &&
!containsNewDotTarget(sourceCode, fn);
if (!isSafe) return false;
if (options.allowNamedFunctions && isNamedFunction(fn)) return false;
if (!options.disallowPrototype && isPrototypeAssignment(sourceCode, fn)) return false;
if (options.singleReturnOnly && !returnsImmediately(fn)) return false;
if (hasNameAndIsExportedAsDefaultExport(fn)) return false;
return true;
};
Loading

0 comments on commit 32c9fa7

Please sign in to comment.