UNPKG

chrome-devtools-frontend

Version:
144 lines (136 loc) 6.41 kB
// 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'); } } } } }; }, });