chrome-devtools-frontend
Version:
Chrome DevTools UI
144 lines (136 loc) • 6.41 kB
text/typescript
// Copyright 2025 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';
export default createRule({
name: 'prefer-sinon-assert',
meta: {
type: 'suggestion',
docs: {
description:
'Prefer `sinon.assert` over `assert` with spy/stub call checks, as it provides a much better error message.',
category: 'Best Practices',
},
messages: {
useSinonAssertInsteadOfAssert:
'Use `sinon.assert.{{ methodName }}(spy)` instead of `assert(spy.{{ methodName }})`',
useSinonAssertCalledInsteadOfAssert:
'Use `sinon.assert.called(spy)` instead of asserting that `spy.notCalled` doesn\'t hold true',
useSinonAssertNotCalledInsteadOfAssert:
'Use `sinon.assert.notCalled(spy)` instead of asserting that `spy.called` doesn\'t hold true',
useSinonAssertCallCountInsteadOfAssert:
'Use `sinon.assert.{{ methodName }}(spy, num)` instead of asserting equality of `spy.callCount` with `num`',
},
fixable: 'code',
schema: [], // no options
},
defaultOptions: [],
create: function(context) {
function isAssert(calleeNode) {
if (calleeNode.type === 'Identifier' && calleeNode.name === 'assert') {
return true;
}
if (calleeNode.type === 'MemberExpression' && calleeNode.object.type === 'Identifier' &&
calleeNode.object.name === 'assert' && calleeNode.property.type === 'Identifier') {
return ['isNotFalse', 'isOk', 'isTrue', 'ok'].includes(calleeNode.property.name);
}
return false;
}
function isAssertFalsy(calleeNode) {
if (calleeNode.type === 'MemberExpression' && calleeNode.object.type === 'Identifier' &&
calleeNode.object.name === 'assert' && calleeNode.property.type === 'Identifier') {
return ['isFalse', 'isNotOk', 'isNotTrue', 'notOk'].includes(calleeNode.property.name);
}
return false;
}
function isAssertEquality(calleeNode) {
if (calleeNode.type === 'MemberExpression' && calleeNode.object.type === 'Identifier' &&
calleeNode.object.name === 'assert' && calleeNode.property.type === 'Identifier') {
return ['deepEqual', 'equal', 'strictEqual'].includes(calleeNode.property.name);
}
return false;
}
function reportError(node, methodName: string, firstArgNodes: TSESTree.Node|TSESTree.Node[], messageId) {
context.report({
node,
messageId,
data: {
methodName,
},
fix(fixer) {
const {sourceCode} = context;
let firstArgText = '';
if (Array.isArray(firstArgNodes)) {
firstArgText = firstArgNodes.map(node => sourceCode.getText(node)).join(', ');
} else {
firstArgText = sourceCode.getText(firstArgNodes);
}
return [
fixer.replaceText(node.arguments[0], firstArgText),
fixer.replaceText(node.callee, `sinon.assert.${methodName}`),
];
}
});
}
return {
CallExpression(node) {
if (node.arguments.length === 1) {
const [argumentNode] = node.arguments;
if (isAssert(node.callee)) {
if (argumentNode.type === 'CallExpression' && argumentNode.callee.type === 'MemberExpression' &&
argumentNode.callee.property.type === 'Identifier') {
const {name} = argumentNode.callee.property;
if ([
'calledOn',
'alwaysCalledOn',
'calledWith',
'calledWithExactly',
'calledOnceWithExactly',
'alwaysCalledWithExactly',
'alwaysCalledWith',
'neverCalledWith',
'calledWithMatch',
'calledOnceWithMatch',
'alwaysCalledWithMatch',
].includes(name)) {
const argumentNodes = [argumentNode.callee.object, ...argumentNode.arguments];
reportError(node, name, argumentNodes, 'useSinonAssertInsteadOfAssert');
}
} else if (argumentNode.type === 'MemberExpression' && argumentNode.property.type === 'Identifier') {
const {name} = argumentNode.property;
if (['notCalled', 'called', 'calledOnce', 'calledTwice', 'calledThrice'].includes(name)) {
reportError(node, name, argumentNode.object, 'useSinonAssertInsteadOfAssert');
}
} else if (argumentNode.type === 'UnaryExpression' && argumentNode.operator === '!') {
const expressionNode = argumentNode.argument;
if (expressionNode.type === 'MemberExpression' && expressionNode.property.type === 'Identifier') {
if (expressionNode.property.name === 'notCalled') {
reportError(node, 'called', expressionNode.object, 'useSinonAssertCalledInsteadOfAssert');
} else if (expressionNode.property.name === 'called') {
reportError(node, 'notCalled', expressionNode.object, 'useSinonAssertNotCalledInsteadOfAssert');
}
}
}
} else if (isAssertFalsy(node.callee)) {
if (argumentNode.type === 'MemberExpression' && argumentNode.property.type === 'Identifier') {
if (argumentNode.property.name === 'notCalled') {
reportError(node, 'called', argumentNode.object, 'useSinonAssertCalledInsteadOfAssert');
} else if (argumentNode.property.name === 'called') {
reportError(node, 'notCalled', argumentNode.object, 'useSinonAssertNotCalledInsteadOfAssert');
}
}
}
} else if (node.arguments.length === 2) {
const [argumentNode] = node.arguments;
if (isAssertEquality(node.callee)) {
if (argumentNode.type === 'MemberExpression' && argumentNode.property.type === 'Identifier' &&
argumentNode.property.name === 'callCount') {
reportError(node, 'callCount', argumentNode.object, 'useSinonAssertCallCountInsteadOfAssert');
}
}
}
}
};
},
});