vscode/.eslint-plugin-local/code-no-reader-after-await.ts
Matt Bierner b8329a3ffc
Run TS eslint rules directly with strip-types
Wth node 20.18, we can now run these typescript files directly instead of having to use ts-node
2025-11-14 14:38:15 -08:00

169 lines
5.2 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TSESTree } from '@typescript-eslint/utils';
import * as eslint from 'eslint';
import type * as ESTree from 'estree';
export default new class NoReaderAfterAwait implements eslint.Rule.RuleModule {
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
return {
'CallExpression': (node: ESTree.CallExpression) => {
const callExpression = node as TSESTree.CallExpression;
if (!isFunctionWithReader(callExpression.callee)) {
return;
}
const functionArg = callExpression.arguments.find(arg =>
arg.type === 'ArrowFunctionExpression' || arg.type === 'FunctionExpression'
) as TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | undefined;
if (!functionArg) {
return;
}
const readerName = getReaderParameterName(functionArg);
if (!readerName) {
return;
}
checkFunctionForAwaitBeforeReader(functionArg, readerName, context);
}
};
}
};
function checkFunctionForAwaitBeforeReader(
fn: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
readerName: string,
context: eslint.Rule.RuleContext
) {
const awaitPositions: { line: number; column: number }[] = [];
const visited = new Set<TSESTree.Node>();
function collectPositions(node: TSESTree.Node) {
if (visited.has(node)) {
return;
}
visited.add(node);
if (node.type === 'AwaitExpression') {
awaitPositions.push({
line: node.loc?.start.line || 0,
column: node.loc?.start.column || 0
});
} else if (node.type === 'CallExpression' && isReaderMethodCall(node, readerName)) {
if (awaitPositions.length > 0) {
const methodName = getMethodName(node);
context.report({
node: node,
message: `Reader method '${methodName}' should not be called after 'await'. The reader becomes invalid after async operations.`
});
}
}
// Safely traverse known node types only
switch (node.type) {
case 'BlockStatement':
node.body.forEach(stmt => collectPositions(stmt));
break;
case 'ExpressionStatement':
collectPositions(node.expression);
break;
case 'VariableDeclaration':
node.declarations.forEach(decl => {
if (decl.init) { collectPositions(decl.init); }
});
break;
case 'AwaitExpression':
if (node.argument) { collectPositions(node.argument); }
break;
case 'CallExpression':
node.arguments.forEach(arg => collectPositions(arg));
break;
case 'IfStatement':
collectPositions(node.test);
collectPositions(node.consequent);
if (node.alternate) { collectPositions(node.alternate); }
break;
case 'TryStatement':
collectPositions(node.block);
if (node.handler) { collectPositions(node.handler.body); }
if (node.finalizer) { collectPositions(node.finalizer); }
break;
case 'ReturnStatement':
if (node.argument) { collectPositions(node.argument); }
break;
case 'BinaryExpression':
case 'LogicalExpression':
collectPositions(node.left);
collectPositions(node.right);
break;
case 'MemberExpression':
collectPositions(node.object);
if (node.computed) { collectPositions(node.property); }
break;
case 'AssignmentExpression':
collectPositions(node.left);
collectPositions(node.right);
break;
}
}
if (fn.body) {
collectPositions(fn.body);
}
}
function getMethodName(callExpression: TSESTree.CallExpression): string {
if (callExpression.callee.type === 'MemberExpression' &&
callExpression.callee.property.type === 'Identifier') {
return callExpression.callee.property.name;
}
return 'read';
}
function isReaderMethodCall(node: TSESTree.CallExpression, readerName: string): boolean {
if (node.callee.type === 'MemberExpression') {
// Pattern 1: reader.read() or reader.readObservable()
if (node.callee.object.type === 'Identifier' &&
node.callee.object.name === readerName &&
node.callee.property.type === 'Identifier') {
return ['read', 'readObservable'].includes(node.callee.property.name);
}
// Pattern 2: observable.read(reader) or observable.readObservable(reader)
if (node.callee.property.type === 'Identifier' &&
['read', 'readObservable'].includes(node.callee.property.name)) {
// Check if the reader is passed as the first argument
return node.arguments.length > 0 &&
node.arguments[0].type === 'Identifier' &&
node.arguments[0].name === readerName;
}
}
return false;
}
const readerFunctions = new Set(['derived', 'autorun', 'autorunOpts', 'autorunHandleChanges', 'autorunSelfDisposable']);
function getReaderParameterName(fn: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression): string | null {
if (fn.params.length === 0) {
return null;
}
const firstParam = fn.params[0];
if (firstParam.type === 'Identifier') {
return firstParam.name;
}
return null;
}
function isFunctionWithReader(callee: TSESTree.Node): boolean {
if (callee.type === 'Identifier') {
return readerFunctions.has(callee.name);
}
return false;
}