chrome-devtools-frontend
Version:
Chrome DevTools UI
155 lines (138 loc) • 5.73 kB
text/typescript
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type {TSESTree} from '@typescript-eslint/utils';
import {createRule} from './utils/ruleCreator.ts';
type Node = TSESTree.Node;
type MemberExpression = TSESTree.MemberExpression;
type BlockStatement = TSESTree.BlockStatement;
type ForStatement = TSESTree.ForStatement;
type ForInStatement = TSESTree.ForInStatement;
type ForOfStatement = TSESTree.ForOfStatement;
type SwitchStatement = TSESTree.SwitchStatement;
type CatchClause = TSESTree.CatchClause;
type StaticBlock = TSESTree.StaticBlock;
type CanvasCall = 'save'|'restore';
export default createRule({
name: 'canvas-context-tracking',
meta: {
type: 'problem',
docs: {
description: 'Track context.save() and context.restores() across scopes',
category: 'Possible Errors',
},
fixable: 'code',
messages: {
saveNotRestored: 'Found a block that has more context.save() calls than context.restore() calls',
uselessRestore: 'Found a context.restore() call with no context.save() prior to it',
},
schema: [], // no options
},
defaultOptions: [],
create: function(context) {
// To track canvas calls across scopes we keep a stack which we push nodes on with every new scope that we find.
// When we then leave a scope, we can check all the calls in that scope and see if they align or not.
let stack: Node[] = [];
// The key is a node's range as a string. The value is a stack of
// context.save calls. When we see a restore, we pop the stack. Therefore,
// if we get to the end of a scope and the stack is not empty, it means the
// user has not balanced their calls correctly.
const scopeToCanvasCalls = new Map<string, CanvasCall[]>();
function nodeToKeyForMap(node: Node): string {
return JSON.stringify(node.range);
}
function enterScope(node: Node): void {
stack.push(node);
}
/**
* Pops the last block scope and checks it for a mismatch of save and restore calls.
*/
function exitScope(): void {
const lastScope = stack.pop();
if (!lastScope) {
return;
}
const stackForCurrentScope = scopeToCanvasCalls.get(
nodeToKeyForMap(lastScope),
);
// We have no issues to report if:
// 1. No calls to save() or restore().
// 2. The amount of save() and restore() calls balanced perfectly, leaving the stack empty.
if (!stackForCurrentScope || stackForCurrentScope.length === 0) {
return;
}
// If we got here it means the stack for the scope has items in, which means that it is unbalanced.
context.report({
node: lastScope,
messageId: 'saveNotRestored',
});
}
/**
* Updates the counter for the current scope.
* @param {'save'|'restore'} methodName
**/
function trackContextCall(methodName: CanvasCall): void {
const currentScopeNode = stack.at(-1);
if (!currentScopeNode) {
return;
}
const currentScopeKey = nodeToKeyForMap(currentScopeNode);
const stackForCurrentScope = scopeToCanvasCalls.get(currentScopeKey) || [];
if (methodName === 'save') {
stackForCurrentScope.push('save');
} else if (methodName === 'restore') {
// If we get a restore() call but the stack is empty, this means that
// we have nothing to restore as we did not save anything in this
// scope. Either the user has forgotten a save() call, or this
// restore() has been accidentally left behind after a refactor and
// should be removed.
if (stackForCurrentScope.length === 0) {
context.report({
messageId: 'uselessRestore',
// Report on the specific call if possible, otherwise the scope.
// The original code reported on currentScopeNode.
node: currentScopeNode,
});
} else {
// Pop the stack, so that the last save() is accounted for.
stackForCurrentScope.pop();
}
}
scopeToCanvasCalls.set(
currentScopeKey,
stackForCurrentScope,
);
}
return {
Program(node) {
stack = [node];
// Initialize map for the program scope
scopeToCanvasCalls.set(nodeToKeyForMap(node), []);
},
MemberExpression(node: MemberExpression) {
const methodCallsToTrack = ['save', 'restore'];
if (node.object.type === 'Identifier' && node.object?.name === 'context' &&
node.property.type === 'Identifier' &&
// Use type assertion because .includes doesn't narrow the type
methodCallsToTrack.includes(node.property?.name as CanvasCall)) {
trackContextCall(node.property.name as CanvasCall);
}
},
// All the different types of scope we have to deal with.
BlockStatement: (node: BlockStatement) => enterScope(node),
'BlockStatement:exit': exitScope,
ForStatement: (node: ForStatement) => enterScope(node),
'ForStatement:exit': exitScope,
ForInStatement: (node: ForInStatement) => enterScope(node),
'ForInStatement:exit': exitScope,
ForOfStatement: (node: ForOfStatement) => enterScope(node),
'ForOfStatement:exit': exitScope,
SwitchStatement: (node: SwitchStatement) => enterScope(node),
'SwitchStatement:exit': exitScope,
CatchClause: (node: CatchClause) => enterScope(node),
'CatchClause:exit': exitScope,
StaticBlock: (node: StaticBlock) => enterScope(node),
'StaticBlock:exit': exitScope,
};
},
});