202 lines
6.5 KiB
JavaScript
202 lines
6.5 KiB
JavaScript
|
|
/**
|
||
|
|
* 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,
|
||
|
|
},
|
||
|
|
};
|