Skip to content

Commit

Permalink
refactor: move writer to a module
Browse files Browse the repository at this point in the history
  • Loading branch information
JamieMason committed Jan 9, 2025
1 parent 32c9fa7 commit 7625a59
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 259 deletions.
7 changes: 7 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TSESTree } from '@typescript-eslint/types';
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';

export type AnyFunctionBody = TSESTree.BlockStatement | TSESTree.Expression;
export type AnyFunction = TSESTree.FunctionDeclaration | TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression;
Expand All @@ -9,6 +10,12 @@ export type WithTypeParameters<T extends AnyFunction> = T & { typeParameters: TS
export type MessageId = keyof typeof MESSAGES_BY_ID;
export type Options = [ActualOptions];

export interface Scope {
isTsx: boolean;
options: ActualOptions;
sourceCode: RuleContext<MessageId, Options>['sourceCode'];
}

export interface ActualOptions {
allowNamedFunctions: boolean;
classPropertiesAllowed: boolean;
Expand Down
357 changes: 177 additions & 180 deletions src/guard.ts
Original file line number Diff line number Diff line change
@@ -1,182 +1,179 @@
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;
import { AnyFunction, AnyFunctionBody, Scope, GeneratorFunction, NamedFunction, WithTypeParameters } from './config';

export class Guard {
isTsx: Scope['isTsx'];
options: Scope['options'];
sourceCode: Scope['sourceCode'];

constructor(scope: Scope) {
this.isTsx = scope.isTsx;
this.options = scope.options;
this.sourceCode = scope.sourceCode;
}

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

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

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

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

returnsImmediately(fn: AnyFunction): boolean {
return this.isBlockStatementWithSingleReturn(fn.body) || this.hasImplicitReturn(fn.body);
}

isExportedAsNamedExport(node: AnyFunction): boolean {
return node.parent.type === AST_NODE_TYPES.ExportNamedDeclaration;
}

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

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

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

isAsyncFunction(node: AnyFunction): boolean {
return node.async === true;
}

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

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
);
}

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

containsSuper(node: TSESTree.Node): boolean {
return this.containsToken('Keyword', 'super', node);
}

containsThis(node: TSESTree.Node): boolean {
return this.containsToken('Keyword', 'this', node);
}

containsArguments(node: TSESTree.Node): boolean {
return this.containsToken('Identifier', 'arguments', node);
}

containsTokenSequence(sequence: [string, string][], node: TSESTree.Node): boolean {
return this.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;
};
}

containsNewDotTarget(node: TSESTree.Node): boolean {
return this.containsTokenSequence(
[
['Keyword', 'new'],
['Punctuator', '.'],
['Identifier', 'target'],
],
node,
);
}

isPrototypeAssignment(node: AnyFunction): boolean {
return this.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;
});
}

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

isNamedFunction(fn: AnyFunction): fn is NamedFunction {
return fn.id !== null && fn.id.name !== null;
}

hasNameAndIsExportedAsDefaultExport(fn: AnyFunction): fn is NamedFunction {
return this.isNamedFunction(fn) && fn.parent.type === AST_NODE_TYPES.ExportDefaultDeclaration;
}

isSafeTransformation(fn: TSESTree.Node): fn is AnyFunction {
const isSafe =
this.isAnyFunction(fn) &&
!this.isGeneratorFunction(fn) &&
!this.isAssertionFunction(fn) &&
!this.isOverloadedFunction(fn) &&
!this.containsThis(fn) &&
!this.containsSuper(fn) &&
!this.containsArguments(fn) &&
!this.containsNewDotTarget(fn);
if (!isSafe) return false;
if (this.options.allowNamedFunctions && this.isNamedFunction(fn)) return false;
if (!this.options.disallowPrototype && this.isPrototypeAssignment(fn)) return false;
if (this.options.singleReturnOnly && !this.returnsImmediately(fn)) return false;
if (this.hasNameAndIsExportedAsDefaultExport(fn)) return false;
return true;
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TSESLint } from '@typescript-eslint/utils';
import { preferArrowFunctions } from './prefer-arrow-functions';
import { preferArrowFunctions } from './rule';

const { name, version } =
// `import`ing here would bypass the TSConfig's `"rootDir": "src"`
Expand Down
Loading

0 comments on commit 7625a59

Please sign in to comment.