UNPKG

chrome-devtools-frontend

Version:
155 lines (138 loc) 5.73 kB
// 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, }; }, });