-
Notifications
You must be signed in to change notification settings - Fork 648
Expand file tree
/
Copy pathcanvas-context-tracking.ts
More file actions
154 lines (138 loc) · 5.56 KB
/
canvas-context-tracking.ts
File metadata and controls
154 lines (138 loc) · 5.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// Copyright 2023 The Chromium Authors
// 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 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,
};
},
});