Files
AI/참고/playwright-main/utils/eslint-plugin-progress/index.js

202 lines
6.5 KiB
JavaScript
Raw Normal View History

2026-05-12 19:40:31 +09:00
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @ts-check
const { ESLintUtils } = require('@typescript-eslint/utils');
const createRule = ESLintUtils.RuleCreator(name => name);
/**
* Checks whether a function parameter named "progress" has type "Progress".
*/
function hasProgressParam(node, services) {
const checker = services.program.getTypeChecker();
for (const param of node.params) {
if (param.type === 'Identifier' && param.name === 'progress') {
const tsNode = services.esTreeNodeToTSNodeMap.get(param);
const type = checker.getTypeAtLocation(tsNode);
if (type.symbol?.name === 'Progress' || type.aliasSymbol?.name === 'Progress')
return true;
// Also check the declared type annotation.
const typeStr = checker.typeToString(type);
if (typeStr === 'Progress')
return true;
}
}
return false;
}
/**
* Checks whether an expression is `progress.race(...)`.
*/
function isProgressRace(node) {
return (
node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'progress' &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'race'
);
}
/**
* Unwraps .then()/.catch()/.finally() chains to get the root call.
*/
function unwrapPromiseChain(node) {
while (node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
node.callee.property.type === 'Identifier' &&
['then', 'catch', 'finally'].includes(node.callee.property.name)) {
node = node.callee.object;
}
return node;
}
/**
* Checks whether a Progress-typed value is passed as first argument to a call,
* unwrapping any .then/.catch/.finally chains.
*/
function passesProgressAsFirstArg(node, services) {
const root = unwrapPromiseChain(node);
if (root.type !== 'CallExpression')
return false;
const firstArg = root.arguments[0];
if (!firstArg)
return false;
const checker = services.program.getTypeChecker();
const tsNode = services.esTreeNodeToTSNodeMap.get(firstArg);
const type = checker.getTypeAtLocation(tsNode);
const typeName = type.symbol?.name || type.aliasSymbol?.name || checker.typeToString(type);
return typeName === 'Progress';
}
/**
* Checks whether the return type of a call expression is a Promise.
*/
function isAsyncCall(node, services) {
const checker = services.program.getTypeChecker();
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
const type = checker.getTypeAtLocation(tsNode);
// Check if the type is a Promise (has a "then" method).
const thenProp = type.getProperty('then');
return !!thenProp;
}
/**
* Walks up to find if this expression is inside a progress.race() call.
*/
function isInsideProgressRace(node) {
let current = node.parent;
while (current) {
if (isProgressRace(current))
return true;
// Stop at function boundaries.
if (current.type === 'ArrowFunctionExpression' || current.type === 'FunctionExpression' || current.type === 'FunctionDeclaration' || current.type === 'MethodDefinition')
return false;
current = current.parent;
}
return false;
}
const rule = createRule({
name: 'await-must-use-progress',
meta: {
type: 'problem',
docs: {
description: 'In methods accepting Progress, awaited async calls must pass progress or be wrapped in progress.race()',
},
messages: {
missingProgress: 'Awaited async call must either pass `progress` as first argument or be wrapped in `progress.race()`. See packages/protocol/src/progress.d.ts.',
},
schema: [],
},
defaultOptions: [],
create(context) {
const services = ESLintUtils.getParserServices(context);
// Stack of functions that have a progress parameter.
const progressFunctionStack = [];
function enterFunction(node) {
progressFunctionStack.push(hasProgressParam(node, services));
}
function exitFunction() {
progressFunctionStack.pop();
}
function isInProgressFunction() {
return progressFunctionStack.length > 0 && progressFunctionStack[progressFunctionStack.length - 1];
}
return {
'FunctionDeclaration': enterFunction,
'FunctionDeclaration:exit': exitFunction,
'FunctionExpression': enterFunction,
'FunctionExpression:exit': exitFunction,
'ArrowFunctionExpression': enterFunction,
'ArrowFunctionExpression:exit': exitFunction,
// Check await expressions in progress functions.
'AwaitExpression'(node) {
if (!isInProgressFunction())
return;
const awaited = node.argument;
// await progress.anything(...) is always fine — calls on the progress object itself.
if (awaited.type === 'CallExpression' &&
awaited.callee.type === 'MemberExpression' &&
awaited.callee.object.type === 'Identifier' &&
awaited.callee.object.name === 'progress')
return;
// await someCall(progress, ...) is fine.
if (passesProgressAsFirstArg(awaited, services))
return;
// Promise.all/race/allSettled/any are aggregation helpers, not async operations themselves.
if (awaited.type === 'CallExpression' &&
awaited.callee.type === 'MemberExpression' &&
awaited.callee.object.type === 'Identifier' &&
awaited.callee.object.name === 'Promise' &&
awaited.callee.property.type === 'Identifier' &&
['all', 'race', 'allSettled', 'any'].includes(awaited.callee.property.name))
return;
// Check if this await is inside a progress.race() call higher up.
if (isInsideProgressRace(node))
return;
// Only flag async calls (calls that return Promise).
if (awaited.type === 'CallExpression' && isAsyncCall(awaited, services)) {
context.report({
node: awaited,
messageId: 'missingProgress',
});
}
},
};
},
});
module.exports = {
rules: {
'await-must-use-progress': rule,
},
};