Repository: avajs/eslint-plugin-ava (js)

Issue:
Rule proposal: Put actual values first and expected values second in assertion
<!-- Issuehunt Badges -->
[<img alt="Issuehunt badges" src="https://img.shields.io/badge/IssueHunt-%2440%20Funded-%2300A156.svg" />](https://issuehunt.io/r/avajs/eslint-plugin-ava/issues/159)
<!-- /Issuehunt Badges -->


When you write assertions, like `t(a, b)`, the first argument is the `value` to evaluate, and the second is the `expected` value to compare it to. But you can actually mix the two arguments and everything would work as expected, except that the error messages will be a bit more confusing because it will swap the expected and received values.

I see it every so often that the arguments are swapped. Not very often, but it can't hurt to have it IMO.

I propose a rule that requires you to put your expressions as the first argument, rather than on the right. Or in other words, as this is probably will be how we'll write the rule, put static expressions on the right.

# Invalid
```js
t.is(1, expression);
t.deepEqual({}, expression);
t.true(1 === foo); // Also handle this? Probably, as power-assert considers `1` as the value
```

# Valid
```js
t.is(expression, 1);
t.deepEqual(expression, {});
t.true(foo === 1);
t.is(expression, otherExpression);
```

Implementation side-note: We could probably use [this function](https://github.com/jfmengels/eslint-ast-utils/blob/master/lib/compute-static-expression.js) to help find out whether an expression is static or not. (I haven't yet published it, but I can do it soonish).

As for the name: `no-expected-left`, `put-expected-right`? :/

---

> *Note: This issue has a bounty, so it's expected that you are an experienced programmer and that you give it your best effort if you intend to tackle this. Don't forget, if applicable, to add tests, docs (double-check for typos). And don't be sloppy. Review your own diff multiple times and try to find ways to improve and simplify your code. Instead of asking too many questions, present solutions. The point of an issue bounty is to reduce my workload, not give me more. Include a 🦄 in your PR description to indicate that you've read this. Thanks for helping out 🙌* - @sindresorhus


<!-- Issuehunt content -->

---

<details>
<summary>
<b>IssueHunt Summary</b>
</summary>


### Backers (Total: $40.00)

- [<img src='https://avatars3.githubusercontent.com/u/44827199?v=4' alt='issuehunt' width=24 height=24> issuehunt](https://issuehunt.io/u/issuehunt) ($40.00)

### Submitted pull Requests
- [#307 Catch & fix out-of-order assertion arguments](https://issuehunt.io/r/avajs/eslint-plugin-ava/pull/307)
---

#### [Become a backer now!](https://issuehunt.io/r/avajs/eslint-plugin-ava/issues/159)
#### [Or submit a pull request to get the deposits!](https://issuehunt.io/r/avajs/eslint-plugin-ava/issues/159)
### Tips

- Checkout the [Issuehunt explorer](https://issuehunt.io/r/avajs/eslint-plugin-ava/) to discover more funded issues.
- Need some help from other developers? [Add your repositories](https://issuehunt.io/r/new) on IssueHunt to raise funds.
</details>
<!-- /Issuehunt content-->

Pull request description:
Catch & fix out-of-order assertion arguments
This PR adds the capability to detect and fix many cases where the user has called an assertion function with the expected value first and the actual value second. Closes #159. This is a breaking change.

If the first argument to `t.deepEqual()`, `t.is()`, `t.not()`, `t.notDeepEqual()`, `t.throws()`, or `t.throwsAsync()` is a static expression, such as `{a: [1, 2, 3]}.['a']`, and the second argument is a non-static expression, then the `assertion-arguments` rule reports an error.

Similarly, if the first argument to `t.assert()`, `t.false()`, `t.falsy()`, `t.true()`, or `t.truthy()` is a binary relation (e.g. `1 < 2`), the left side of the relation is a static expression, and the right side of the relation is a non-static expression, then the `assertion-arguments` rule reports an error.

These errors are usually fixable; the autofix swaps the first and second arguments to the assertion. For the single-argument assertions, the relational operator is inverted if appropriate. They are not fixable if the autofix would change the relationship between either argument and any comment.

This should be a sound means of detecting expected arguments because supplying a static actual argument asserts nothing about the code under test. That is, `t.deepEqual({a: 1}, expected);` tests nothing.

Static values are computed using [`eslint-utils`](https://github.com/mysticatea/eslint-utils)' [`getStaticValue()`](https://eslint-utils.mysticatea.dev/api/ast-utils.html#getstaticvalue) function, which seems quite comprehensive. `eslint-utils` was already in the dependency graph of `eslint-plugin-ava`; this PR lifts it to top-level.

The PR incidentally adds `assertion-arguments` support for `t.throwsAsync()`, `t.notThrowsAsync()`, and `t.assert()`. `t.throwsAsync()` and `t.assert()` are compatible with the new functionality.

Support for `t.like()` is blocked by PR #306. Merging #306 will enable `t.like()` support, and [this branch](https://github.com/ninevra/eslint-plugin-ava/tree/assertion-argument-order-like-support) can then add tests for `t.like()` support.

Potentially interesting design decisions:
- Extend `assertion-arguments` rather than adding a new rule, and don't provide an option to disable the check. This is per the [previous discussion](https://github.com/avajs/eslint-plugin-ava/issues/159#issuecomment-261474276) in #159.
- Do support single-argument assertions when the argument is a binary relation. This is motivated by `power-assert`, per the proposal in #159.
- Don't autofix cases where any comments are present around the target values. For example, `t.is(/* left comment */ 'static', dynamic /* right comment */)` produces an error, but does not provide a fix.
  - Moving text around in the presence of comments is dangerous and complex.
    - Line comments (`// comment`) must not be placed before any other tokens.
    - Flow type cast comments _must_ be moved with their associated value, or else they'll end up casting the other argument.
    - Eslint directive comments _must not_ be moved, or else suffer various kinds of missense. `/* eslint-disable */` and `/* eslint-enable */` are particularly problematic.
    - Other directive comments might have arbitrary behavior; enumerating, detecting, and handling them all is impossible.
- Don't consider function-valued expressions to be static. This is useful, though not strictly necessary, in supporting `t.throws()` and `t.throwsAsync()`, where the thrower (analogous to actual) is often a function. In `t.throws(() => {}, TypeError);` the left argument is static-valued (technically, all function expressions which don't close over dynamic values are static-valued) and the right is non-static. There is a mistake here, in that `() => {}` tests nothing, but the arguments are clearly not out of order. Currently, `eslint-utils#getStaticValue()` doesn't compute static-valued functions, but I've added a type-check in case a future minor version of it does.
- Don't consider well-known identifiers (`undefined`, `NaN`, `Infinity`) to be static. These are non-configurable, non-writable data properties of the global object, and can be shadowed in nested scopes by non-static values.

Especially questionable design decisions:
- Do consider expressions with side effects (e.g. `a = 1` or `void a()`) to be static.
  - The principle that static-valued expressions shouldn't be passed as actual arguments is still sound here, but expected arguments also shouldn't have side effects, so something weirder than out-of-order arguments might be going on in these cases.
  - Detection of side effects is available in `eslint-utils`, but it either has false positives (reporting side effects on pure static MemberExpresssions) or false negatives (reporting `void ident.prop` as pure, even though `ident.prop` could be a getter or proxy trap), depending on configuration.

Potential further improvements:
- Track variable identifiers back to their declaration to find a static value.
  - Only non-shadowed non-configurable immutable globals and non-shadowed `const`-declared immutable values could feasibly be proven static.
  - This could enable detection of many common, likely static values, such as `undefined` and `const expected = 'static'; t.is(expected, actual);`.
  - This functionality is offered by `eslint-utils`; however, its implementation has multiple major issues.
    - The algorithm used by `eslint-utils` trusts the user not to redefine globals like `undefined` or `RegExp`. If the user does this, some identifiers will be erroneously treated as static, and reports might be issued in error.
    - The algorithm used by `eslint-utils` treats mutable values as static whenever the initial declaration is static, and doesn't check for future modifications to the value. So, `const dynamic = {a: 1}; dynamic.b = foo();` is erroneously reported as static. This could cause reports to be issued in error.
    - For some reason, enabling scope resolution in `eslint-utils` breaks the `p-queue` integration test; linting `p-queue`'s `test/test.ts` thereafter errors with ``Expected `index` to be a number.`` `Occurred while linting <...>/p-queue/test/test.ts:672`. I have not been able to identify the cause.

🦄


<!-- Issuehunt content -->

---

<details>
<summary>
<b>IssueHunt Summary</b>
</summary>

### Referenced issues

This pull request has been submitted to:
- [#159: Rule proposal: Put actual values first and expected values second in assertion](https://issuehunt.io/repos/49722492/issues/159)
---
</details>
<!-- /Issuehunt content-->

Patch:
diff --git a/docs/rules/assertion-arguments.md b/docs/rules/assertion-arguments.md
index 32daff2..686f2ff 100644
--- a/docs/rules/assertion-arguments.md
+++ b/docs/rules/assertion-arguments.md
@@ -6,6 +6,8 @@ Enforces passing the right number of arguments to assertion methods like `t.is()
 
 Assertion messages are optional arguments that can be given to any assertion call to improve the error message, should the assertion fail.
 
+This rule also attempts to enforce passing actual values before expected values. If exactly one of the first two arguments to a two-argument assertion is a static expression such as `{a: 1}`, then the static expression must come second. (`t.regex()` and `t.notRegex()` are excluded from this check, because either their `contents` argument or their `regex` argument could plausibly be the actual or expected value.) If the argument to a one-argument assertion is a binary relation such as `'static' === dynamic`, a similar check is performed on its left- and right-hand sides. Errors of these kinds are usually fixable.
+
 ## Fail
 
 ```js
diff --git a/package.json b/package.json
index b76650a..5a39528 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
 	"dependencies": {
 		"deep-strict-equal": "^0.2.0",
 		"enhance-visitors": "^1.0.0",
+		"eslint-utils": "^2.1.0",
 		"espree": "^7.1.0",
 		"espurify": "^2.0.1",
 		"import-modules": "^2.0.0",
diff --git a/rules/assertion-arguments.js b/rules/assertion-arguments.js
index a6c1494..f668e78 100644
--- a/rules/assertion-arguments.js
+++ b/rules/assertion-arguments.js
@@ -1,9 +1,14 @@
 'use strict';
 const {visitIf} = require('enhance-visitors');
+const {getStaticValue, isOpeningParenToken, isCommaToken} = require('eslint-utils');
 const util = require('../util');
 const createAvaRule = require('../create-ava-rule');
 
 const expectedNbArguments = {
+	assert: {
+		min: 1,
+		max: 2
+	},
 	deepEqual: {
 		min: 2,
 		max: 3
@@ -44,6 +49,10 @@ const expectedNbArguments = {
 		min: 1,
 		max: 2
 	},
+	notThrowsAsync: {
+		min: 1,
+		max: 2
+	},
 	pass: {
 		min: 0,
 		max: 1
@@ -72,6 +81,10 @@ const expectedNbArguments = {
 		min: 1,
 		max: 3
 	},
+	throwsAsync: {
+		min: 1,
+		max: 3
+	},
 	true: {
 		min: 1,
 		max: 2
@@ -86,6 +99,111 @@ const expectedNbArguments = {
 	}
 };
 
+const actualExpectedAssertions = new Set([
+	'deepEqual',
+	'is',
+	'like',
+	'not',
+	'notDeepEqual',
+	'throws',
+	'throwsAsync'
+]);
+
+const relationalActualExpectedAssertions = new Set([
+	'assert',
+	'truthy',
+	'falsy',
+	'true',
+	'false'
+]);
+
+const comparisonOperators = new Map([
+	['>', '<'],
+	['>=', '<='],
+	['==', '=='],
+	['===', '==='],
+	['!=', '!='],
+	['!==', '!=='],
+	['<=', '>='],
+	['<', '>']
+]);
+
+const flipOperator = operator => comparisonOperators.get(operator);
+
+function isStatic(node) {
+	const staticValue = getStaticValue(node);
+	return staticValue !== null && typeof staticValue.value !== 'function';
+}
+
+function * sourceRangesOfArguments(sourceCode, callExpression) {
+	const openingParen = sourceCode.getTokenAfter(
+		callExpression.callee,
+		{filter: token => isOpeningParenToken(token)}
+	);
+
+	const closingParen = sourceCode.getLastToken(callExpression);
+
+	for (const [index, argument] of callExpression.arguments.entries()) {
+		const previousToken = index === 0 ?
+			openingParen :
+			sourceCode.getTokenBefore(
+				argument,
+				{filter: token => isCommaToken(token)}
+			);
+
+		const nextToken = index === callExpression.arguments.length - 1 ?
+			closingParen :
+			sourceCode.getTokenAfter(
+				argument,
+				{filter: token => isCommaToken(token)}
+			);
+
+		const firstToken = sourceCode.getTokenAfter(
+			previousToken,
+			{includeComments: true}
+		);
+
+		const lastToken = sourceCode.getTokenBefore(
+			nextToken,
+			{includeComments: true}
+		);
+
+		yield [firstToken.range[0], lastToken.range[1]];
+	}
+}
+
+function sourceOfBinaryExpressionComponents(sourceCode, node) {
+	const {operator, left, right} = node;
+
+	const operatorToken = sourceCode.getFirstTokenBetween(
+		left,
+		right,
+		{filter: token => token.value === operator}
+	);
+
+	const previousToken = sourceCode.getTokenBefore(node);
+	const nextToken = sourceCode.getTokenAfter(node);
+
+	const leftRange = [
+		sourceCode.getTokenAfter(previousToken, {includeComments: true}).range[0],
+		sourceCode.getTokenBefore(operatorToken, {includeComments: true}).range[1]
+	];
+
+	const rightRange = [
+		sourceCode.getTokenAfter(operatorToken, {includeComments: true}).range[0],
+		sourceCode.getTokenBefore(nextToken, {includeComments: true}).range[1]
+	];
+
+	return [leftRange, operatorToken, rightRange];
+}
+
+function noComments(sourceCode, ...nodes) {
+	return nodes.every(node => {
+		const {leading, trailing} = sourceCode.getComments(node);
+		return leading.length === 0 && trailing.length === 0;
+	});
+}
+
 const create = context => {
 	const ava = createAvaRule();
 	const options = context.options[0] || {};
@@ -141,19 +259,102 @@ const create = context => {
 				report(node, `Not enough arguments. Expected at least ${nArgs.min}.`);
 			} else if (node.arguments.length > nArgs.max) {
 				report(node, `Too many arguments. Expected at most ${nArgs.max}.`);
-			} else if (enforcesMessage && nArgs.min !== nArgs.max) {
-				const hasMessage = gottenArgs === nArgs.max;
+			} else {
+				if (enforcesMessage && nArgs.min !== nArgs.max) {
+					const hasMessage = gottenArgs === nArgs.max;
 
-				if (!hasMessage && shouldHaveMessage) {
-					report(node, 'Expected an assertion message, but found none.');
-				} else if (hasMessage && !shouldHaveMessage) {
-					report(node, 'Expected no assertion message, but found one.');
+					if (!hasMessage && shouldHaveMessage) {
+						report(node, 'Expected an assertion message, but found none.');
+					} else if (hasMessage && !shouldHaveMessage) {
+						report(node, 'Expected no assertion message, but found one.');
+					}
 				}
+
+				checkArgumentOrder({node, assertion: members[0], context});
 			}
 		})
 	});
 };
 
+function checkArgumentOrder({node, assertion, context}) {
+	const [first, second] = node.arguments;
+	if (actualExpectedAssertions.has(assertion) && second) {
+		const [leftNode, rightNode] = [first, second];
+		if (isStatic(leftNode) && !isStatic(rightNode)) {
+			context.report(
+				makeOutOfOrder2ArgumentReport({node, leftNode, rightNode, context})
+			);
+		}
+	} else if (
+		relationalActualExpectedAssertions.has(assertion) &&
+		first &&
+		first.type === 'BinaryExpression' &&
+		comparisonOperators.has(first.operator)
+	) {
+		const [leftNode, rightNode] = [first.left, first.right];
+		if (isStatic(leftNode) && !isStatic(rightNode)) {
+			context.report(
+				makeOutOfOrder1ArgumentReport({node: first, leftNode, rightNode, context})
+			);
+		}
+	}
+}
+
+function makeOutOfOrder2ArgumentReport({node, leftNode, rightNode, context}) {
+	const sourceCode = context.getSourceCode();
+	const [leftRange, rightRange] = sourceRangesOfArguments(sourceCode, node);
+	const report = {
+		message: 'Expected values should come after actual values.',
+		loc: {
+			start: sourceCode.getLocFromIndex(leftRange[0]),
+			end: sourceCode.getLocFromIndex(rightRange[1])
+		}
+	};
+
+	if (noComments(sourceCode, leftNode, rightNode)) {
+		report.fix = fixer => {
+			const leftText = sourceCode.getText().slice(...leftRange);
+			const rightText = sourceCode.getText().slice(...rightRange);
+			return [
+				fixer.replaceTextRange(leftRange, rightText),
+				fixer.replaceTextRange(rightRange, leftText)
+			];
+		};
+	}
+
+	return report;
+}
+
+function makeOutOfOrder1ArgumentReport({node, leftNode, rightNode, context}) {
+	const sourceCode = context.getSourceCode();
+	const [
+		leftRange,
+		operatorToken,
+		rightRange
+	] = sourceOfBinaryExpressionComponents(sourceCode, node);
+	const report = {
+		message: 'Expected values should come after actual values.',
+		loc: {
+			start: sourceCode.getLocFromIndex(leftRange[0]),
+			end: sourceCode.getLocFromIndex(rightRange[1])
+		}
+	};
+
+	if (noComments(sourceCode, leftNode, rightNode, node)) {
+		report.fix = fixer => {
+			const leftText = sourceCode.getText().slice(...leftRange);
+			const rightText = sourceCode.getText().slice(...rightRange);
+			return [
+				fixer.replaceTextRange(leftRange, rightText),
+				fixer.replaceText(operatorToken, flipOperator(node.operator)),
+				fixer.replaceTextRange(rightRange, leftText)
+			];
+		};
+	}
+
+	return report;
+}
+
 const schema = [{
 	type: 'object',
 	properties: {
@@ -170,6 +371,7 @@ const schema = [{
 module.exports = {
 	create,
 	meta: {
+		fixable: 'code',
 		docs: {
 			url: util.getDocsUrl(__filename)
 		},


Test patch:
diff --git a/test/assertion-arguments.js b/test/assertion-arguments.js
index ed6c1cd..5c256cc 100644
--- a/test/assertion-arguments.js
+++ b/test/assertion-arguments.js
@@ -5,6 +5,9 @@ const rule = require('../rules/assertion-arguments');
 const ruleTester = avaRuleTester(test, {
 	env: {
 		es6: true
+	},
+	parserOptions: {
+		ecmaVersion: 2020
 	}
 });
 
@@ -12,29 +15,149 @@ const missingError = 'Expected an assertion message, but found none.';
 const foundError = 'Expected no assertion message, but found one.';
 const tooFewError = n => `Not enough arguments. Expected at least ${n}.`;
 const tooManyError = n => `Too many arguments. Expected at most ${n}.`;
+const outOfOrderError = (line, column, endLine, endColumn) => ({
+	message: 'Expected values should come after actual values.',
+	line, column, endLine, endColumn
+});
 
 const header = 'const test = require(\'ava\');';
 
-function testCase(message, content, errorMessage, useHeader) {
+function testCode(content, useHeader) {
 	const testFn = `
 		test(t => {
 			${content}
 		});
 	`;
+	const code = (useHeader === false ? '' : header) + testFn;
+	return code;
+}
+
+function offsetError(error, line, column) {
+	const offset = {...error};
+
+	if (error.line !== undefined) {
+		offset.line += line;
+	}
+
+	if (error.column !== undefined && error.line === 1) {
+		offset.column += column;
+	}
+
+	if (error.endLine !== undefined) {
+		offset.endLine += line;
+	}
+
+	if (error.endColumn !== undefined && error.endLine === 1) {
+		offset.endColumn += column;
+	}
+
+	return offset;
+}
+
+function testCase(message, content, errors = [], {
+	useHeader, output = null
+} = {}) {
+	if (!Array.isArray(errors)) {
+		errors = [errors];
+	}
+
+	const offset = useHeader === false ? [1, 3] : [2, 3];
+
+	errors = errors
+		.map(error => typeof error === 'string' ? {message: error} : error)
+		.map(error => ({ruleId: 'assertion-arguments', ...error}))
+		.map(error => offsetError(error, ...offset));
 
 	return {
-		errors: errorMessage && [{
-			ruleId: 'assertion-arguments',
-			message: errorMessage
-		}],
+		errors,
 		options: message ? [{message}] : [],
-		code: (useHeader === false ? '' : header) + testFn
+		code: testCode(content, useHeader),
+		output: output === null ? null : testCode(output, useHeader)
 	};
 }
 
+const statics = [
+	'null',
+	'true',
+	'false',
+	'1.0',
+	'1n',
+	'"string"',
+	/* eslint-disable no-template-curly-in-string */
+	'`template ${"string"}`',
+	/* eslint-enable no-template-curly-in-string */
+	'/.*regex.*\\.js/ig',
+	'{}',
+	'{a: 1}',
+	'{a: {b: 1}}',
+	'{"c": 1}',
+	'{3: 1}',
+	'{["a" + "b"]: {c: 1}}',
+	'{...{a: 1}}',
+	'[]',
+	'[1, 2, {a: 3}]',
+	'[...[1, 2, 3]]',
+	'void 0',
+	'void a',
+	'~1',
+	'!""',
+	'+[]',
+	'-"a"',
+	'1 + "a"',
+	'1 && 0',
+	'null ?? false',
+	'true ? [] : [1]',
+	'true ? [] : a',
+	'{a: 1}.a',
+	'{a: 1}["a"]',
+	'{a: 1}.a["a"]',
+	'[1][0]',
+	'[[1]][0][0]',
+	'{a: 1}?.a?.["b"]',
+	'[{a: 1}]?.a?.[0]',
+	'a = 1'
+];
+
+const dynamics = [
+	'NaN',
+	'undefined',
+	'Infinity',
+	'-Infinity',
+	'a',
+	'a.b',
+	'a["b"]',
+	'{}[a]',
+	'{}.a[a]',
+	'[][a]',
+	'[[1]][0][a]',
+	'(() => {}).a',
+	'a()',
+	'new A()',
+	'class A {}',
+	'function a() {}',
+	'() => {}',
+	'tagged`template string`',
+	'new RegExp(\'[dynamic]\')',
+	'~a',
+	'++a',
+	'delete a.b',
+	'[delete a.b]',
+	'{...a}',
+	'{[a]: 1}',
+	'{ a() {} }',
+	'{ get a() {}}',
+	'{ set a(value) {} }',
+	'[...a]',
+	'a ? [] : [1]',
+	'true ? a : [1]',
+	'"a"?.()',
+	'{a: 1}?.[b]'
+];
+
 ruleTester.run('assertion-arguments', rule, {
 	valid: [
 		testCase(false, 't.plan(1);'),
+		testCase(false, 't.assert(true, \'message\');'),
 		testCase(false, 't.deepEqual({}, {}, \'message\');'),
 		testCase(false, 't.fail(\'message\');'),
 		testCase(false, 't.false(false, \'message\');'),
@@ -46,11 +169,13 @@ ruleTester.run('assertion-arguments', rule, {
 		testCase(false, 't.not(\'not\', \'same\', \'message\');'),
 		testCase(false, 't.notDeepEqual({}, {a: true}, \'message\');'),
 		testCase(false, 't.notThrows(Promise.resolve(), \'message\');'),
+		testCase(false, 't.notThrowsAsync(Promise.resolve(), \'message\');'),
 		testCase(false, 't.pass(\'message\');'),
 		testCase(false, 't.regex(a, /a/, \'message\');'),
 		testCase(false, 't.notRegex(a, /a/, \'message\');'),
 		testCase(false, 't.skip.is(\'same\', \'same\', \'message\');'),
 		testCase(false, 't.throws(Promise.reject(), Error, \'message\');'),
+		testCase(false, 't.throwsAsync(Promise.reject(), Error, \'message\');'),
 		testCase(false, 't.true(true, \'message\');'),
 		testCase(false, 't.truthy(\'unicorn\', \'message\');'),
 		testCase(false, 't.snapshot(value, \'message\');'),
@@ -59,8 +184,9 @@ ruleTester.run('assertion-arguments', rule, {
 		testCase(false, 't.timeout(100, \'message\');'),
 		testCase(false, 'foo.t.plan();'),
 		// Shouldn't be triggered since it's not a test file
-		testCase(false, 't.true(true);', false, false),
+		testCase(false, 't.true(true);', false, {useHeader: false}),
 
+		testCase(false, 't.assert(true);'),
 		testCase(false, 't.deepEqual({}, {});'),
 		testCase(false, 't.fail();'),
 		testCase(false, 't.false(false);'),
@@ -72,23 +198,27 @@ ruleTester.run('assertion-arguments', rule, {
 		testCase(false, 't.not(\'not\', \'same\');'),
 		testCase(false, 't.notDeepEqual({}, {a: true});'),
 		testCase(false, 't.notThrows(Promise.resolve());'),
+		testCase(false, 't.notThrowsAsync(Promise.resolve());'),
 		testCase(false, 't.pass();'),
 		testCase(false, 't.regex(a, /a/);'),
 		testCase(false, 't.notRegex(a, /a/);'),
 		testCase(false, 't.skip.is(\'same\', \'same\');'),
 		testCase(false, 't.throws(Promise.reject());'),
 		testCase(false, 't.throws(Promise.reject(), Error);'),
+		testCase(false, 't.throwsAsync(Promise.reject());'),
+		testCase(false, 't.throwsAsync(Promise.reject(), Error);'),
 		testCase(false, 't.true(true);'),
 		testCase(false, 't.truthy(\'unicorn\');'),
 		testCase(false, 't.snapshot(value);'),
 		// Shouldn't be triggered since it's not a test file
-		testCase(false, 't.true(true, \'message\');', [], false),
+		testCase(false, 't.true(true, \'message\');', [], {useHeader: false}),
 
 		testCase(false, 't.context.a(1, 2, 3, 4);'),
 		testCase(false, 't.context.is(1, 2, 3, 4);'),
 		testCase(false, 't.foo(1, 2, 3, 4);'),
 
 		testCase('always', 't.plan(1);'),
+		testCase('always', 't.assert(true, \'message\');'),
 		testCase('always', 't.pass(\'message\');'),
 		testCase('always', 't.fail(\'message\');'),
 		testCase('always', 't.truthy(\'unicorn\', \'message\');'),
@@ -102,6 +232,8 @@ ruleTester.run('assertion-arguments', rule, {
 		testCase('always', 't.like({}, {}, \'message\');'),
 		testCase('always', 't.throws(Promise.reject(), Error, \'message\');'),
 		testCase('always', 't.notThrows(Promise.resolve(), \'message\');'),
+		testCase('always', 't.throwsAsync(Promise.reject(), Error, \'message\');'),
+		testCase('always', 't.notThrowsAsync(Promise.resolve(), \'message\');'),
 		testCase('always', 't.regex(a, /a/, \'message\');'),
 		testCase('always', 't.notRegex(a, /a/, \'message\');'),
 		testCase('always', 't.ifError(new Error(), \'message\');'),
@@ -115,13 +247,14 @@ ruleTester.run('assertion-arguments', rule, {
 		testCase('always', 't.try(\'title\', tt => tt.pass(), 1, 2);'),
 
 		// Shouldn't be triggered since it's not a test file
-		testCase('always', 't.true(true);', [], false),
+		testCase('always', 't.true(true);', [], {useHeader: false}),
 
 		testCase('always', 't.context.a(1, 2, 3, 4);'),
 		testCase('always', 't.context.is(1, 2, 3, 4);'),
 		testCase('always', 't.foo(1, 2, 3, 4);'),
 
 		testCase('never', 't.plan(1);'),
+		testCase('never', 't.assert(true);'),
 		testCase('never', 't.pass();'),
 		testCase('never', 't.fail();'),
 		testCase('never', 't.truthy(\'unicorn\');'),
@@ -136,6 +269,9 @@ ruleTester.run('assertion-arguments', rule, {
 		testCase('never', 't.throws(Promise.reject());'),
 		testCase('never', 't.throws(Promise.reject(), Error);'),
 		testCase('never', 't.notThrows(Promise.resolve());'),
+		testCase('never', 't.throwsAsync(Promise.reject());'),
+		testCase('never', 't.throwsAsync(Promise.reject(), Error);'),
+		testCase('never', 't.notThrowsAsync(Promise.resolve());'),
 		testCase('never', 't.regex(a, /a/);'),
 		testCase('never', 't.notRegex(a, /a/);'),
 		testCase('never', 't.ifError(new Error());'),
@@ -149,7 +285,7 @@ ruleTester.run('assertion-arguments', rule, {
 		testCase('never', 't.try(\'title\', tt => tt.pass(), 1, 2);'),
 
 		// Shouldn't be triggered since it's not a test file
-		testCase('never', 't.true(true, \'message\');', [], false),
+		testCase('never', 't.true(true, \'message\');', [], {useHeader: false}),
 
 		testCase('never', 't.context.a(1, 2, 3, 4);'),
 		testCase('never', 't.context.is(1, 2, 3, 4);'),
@@ -173,11 +309,35 @@ ruleTester.run('assertion-arguments', rule, {
 		testCase('never', 't.end.skip();'),
 		testCase('never', 't.end.skip(error);'),
 		testCase('never', 't.skip.end();'),
-		testCase('never', 't.skip.end(error);')
+		testCase('never', 't.skip.end(error);'),
+
+		// Assertion argument order
+		testCase(false, 't.deepEqual(\'static\', \'static\');'),
+		testCase(false, 't.deepEqual(dynamic, \'static\');'),
+		testCase(false, 't.deepEqual(dynamic, dynamic);'),
+		testCase(false, 't.is(dynamic, \'static\');'),
+		testCase(false, 't.like(dynamic, \'static\');'),
+		testCase(false, 't.not(dynamic, \'static\');'),
+		testCase(false, 't.notDeepEqual(dynamic, \'static\');'),
+		testCase(false, 't.throws(() => {}, expected);'),
+		testCase(false, 't.throws(() => {}, null, "message");'),
+		testCase(false, 't.throwsAsync(async () => {}, {name: \'TypeError\'});'),
+		testCase(false, 't.throwsAsync(async () => {}, null, "message");'),
+		testCase(false, 't.assert(dynamic === \'static\');'),
+		testCase(false, 't.true(dynamic === \'static\');'),
+		testCase(false, 't.false(dynamic === \'static\');'),
+		testCase(false, 't.truthy(dynamic === \'static\');'),
+		testCase(false, 't.falsy(dynamic === \'static\');'),
+		// No documented actual/expected distinction for t.regex()
+		testCase(false, 't.regex(\'static\', new RegExp(\'[dynamic]+\'));'),
+		testCase(false, 't.regex(dynamic, /[static]/);'),
+		testCase(false, 't.notRegex(\'static\', new RegExp(\'[dynamic]+\'));'),
+		testCase(false, 't.notRegex(dynamic, /[static]/);')
 	],
 	invalid: [
 		// Not enough arguments
 		testCase(false, 't.plan();', tooFewError(1)),
+		testCase(false, 't.assert();', tooFewError(1)),
 		testCase(false, 't.truthy();', tooFewError(1)),
 		testCase(false, 't.falsy();', tooFewError(1)),
 		testCase(false, 't.true();', tooFewError(1)),
@@ -189,6 +349,8 @@ ruleTester.run('assertion-arguments', rule, {
 		testCase(false, 't.like({});', tooFewError(2)),
 		testCase(false, 't.throws();', tooFewError(1)),
 		testCase(false, 't.notThrows();', tooFewError(1)),
+		testCase(false, 't.throwsAsync();', tooFewError(1)),
+		testCase(false, 't.notThrowsAsync();', tooFewError(1)),
 		testCase(false, 't.regex(a);', tooFewError(2)),
 		testCase(false, 't.notRegex(a);', tooFewError(2)),
 		testCase(false, 't.ifError();', tooFewError(1)),
@@ -201,6 +363,7 @@ ruleTester.run('assertion-arguments', rule, {
 
 		// Too many arguments
 		testCase(false, 't.plan(1, \'extra argument\');', tooManyError(1)),
+		testCase(false, 't.assert(true, \'message\', \'extra argument\');', tooManyError(2)),
 		testCase(false, 't.pass(\'message\', \'extra argument\');', tooManyError(1)),
 		testCase(false, 't.fail(\'message\', \'extra argument\');', tooManyError(1)),
 		testCase(false, 't.truthy(\'unicorn\', \'message\', \'extra argument\');', tooManyError(2)),
@@ -214,6 +377,8 @@ ruleTester.run('assertion-arguments', rule, {
 		testCase(false, 't.like({}, {}, \'message\', \'extra argument\');', tooManyError(3)),
 		testCase(false, 't.throws(Promise.reject(), Error, \'message\', \'extra argument\');', tooManyError(3)),
 		testCase(false, 't.notThrows(Promise.resolve(), \'message\', \'extra argument\');', tooManyError(2)),
+		testCase(false, 't.throwsAsync(Promise.reject(), Error, \'message\', \'extra argument\');', tooManyError(3)),
+		testCase(false, 't.notThrowsAsync(Promise.resolve(), \'message\', \'extra argument\');', tooManyError(2)),
 		testCase(false, 't.regex(a, /a/, \'message\', \'extra argument\');', tooManyError(3)),
 		testCase(false, 't.notRegex(a, /a/, \'message\', \'extra argument\');', tooManyError(3)),
 		testCase(false, 't.ifError(new Error(), \'message\', \'extra argument\');', tooManyError(2)),
@@ -223,6 +388,7 @@ ruleTester.run('assertion-arguments', rule, {
 		testCase(false, 't.teardown(() => {}, \'extra argument\');', tooManyError(1)),
 		testCase(false, 't.timeout(1, \'message\', \'extra argument\');', tooManyError(2)),
 
+		testCase('always', 't.assert(true);', missingError),
 		testCase('always', 't.pass();', missingError),
 		testCase('always', 't.fail();', missingError),
 		testCase('always', 't.truthy(\'unicorn\');', missingError),
@@ -237,12 +403,17 @@ ruleTester.run('assertion-arguments', rule, {
 		testCase('always', 't.throws(Promise.reject());', missingError),
 		testCase('always', 't.throws(Promise.reject(), Error);', missingError),
 		testCase('always', 't.notThrows(Promise.resolve());', missingError),
+		testCase('always', 't.throwsAsync(Promise.reject());', missingError),
+		testCase('always', 't.throwsAsync(Promise.reject(), Error);', missingError),
+		testCase('always', 't.notThrowsAsync(Promise.resolve());', missingError),
 		testCase('always', 't.regex(a, /a/);', missingError),
 		testCase('always', 't.notRegex(a, /a/);', missingError),
 		testCase('always', 't.ifError(new Error());', missingError),
 		testCase('always', 't.skip.is(\'same\', \'same\');', missingError),
 		testCase('always', 't.is.skip(\'same\', \'same\');', missingError),
 		testCase('always', 't.snapshot(value);', missingError),
+
+		testCase('never', 't.assert(true, \'message\');', foundError),
 		testCase('never', 't.pass(\'message\');', foundError),
 		testCase('never', 't.fail(\'message\');', foundError),
 		testCase('never', 't.truthy(\'unicorn\', \'message\');', foundError),
@@ -256,6 +427,8 @@ ruleTester.run('assertion-arguments', rule, {
 		testCase('never', 't.like({}, {}, \'message\');', foundError),
 		testCase('never', 't.throws(Promise.reject(), Error, \'message\');', foundError),
 		testCase('never', 't.notThrows(Promise.resolve(), \'message\');', foundError),
+		testCase('never', 't.throwsAsync(Promise.reject(), Error, \'message\');', foundError),
+		testCase('never', 't.notThrowsAsync(Promise.resolve(), \'message\');', foundError),
 		testCase('never', 't.regex(a, /a/, \'message\');', foundError),
 		testCase('never', 't.notRegex(a, /a/, \'message\');', foundError),
 		testCase('never', 't.ifError(new Error(), \'message\');', foundError),
@@ -265,6 +438,107 @@ ruleTester.run('assertion-arguments', rule, {
 
 		testCase(false, 't.end(\'too many\', \'arguments\');', tooManyError(1)),
 		testCase(false, 't.skip.end(\'too many\', \'arguments\');', tooManyError(1)),
-		testCase(false, 't.end.skip(\'too many\', \'arguments\');', tooManyError(1))
+		testCase(false, 't.end.skip(\'too many\', \'arguments\');', tooManyError(1)),
+
+		// Assertion argument order
+		testCase(false, 't.deepEqual(\'static\', dynamic);',
+			outOfOrderError(1, 13, 1, 30),
+			{output: 't.deepEqual(dynamic, \'static\');'}
+		),
+		testCase(false, 't.is(\'static\', dynamic);',
+			outOfOrderError(1, 6, 1, 23),
+			{output: 't.is(dynamic, \'static\');'}
+		),
+		testCase(false, 't.like({a: {b: 1}}, dynamic);',
+			outOfOrderError(1, 8, 1, 28),
+			{output: 't.like(dynamic, {a: {b: 1}});'}
+		),
+		testCase(false, 't.not(\'static\', dynamic);',
+			outOfOrderError(1, 7, 1, 24),
+			{output: 't.not(dynamic, \'static\');'}
+		),
+		testCase(false, 't.notDeepEqual({static: true}, dynamic);',
+			outOfOrderError(1, 16, 1, 39),
+			{output: 't.notDeepEqual(dynamic, {static: true});'}
+		),
+		testCase(false, 't.throws({name: \'TypeError\'}, () => {});',
+			outOfOrderError(1, 10, 1, 39),
+			{output: 't.throws(() => {}, {name: \'TypeError\'});'}
+		),
+		testCase(false, 't.throwsAsync({name: \'TypeError\'}, async () => {});',
+			outOfOrderError(1, 15, 1, 50),
+			{output: 't.throwsAsync(async () => {}, {name: \'TypeError\'});'}
+		),
+		testCase('always', 't.deepEqual({}, actual, \'message\');',
+			outOfOrderError(1, 13, 1, 23),
+			{output: 't.deepEqual(actual, {}, \'message\');'}
+		),
+		testCase('never', 't.deepEqual({}, actual);',
+			outOfOrderError(1, 13, 1, 23),
+			{output: 't.deepEqual(actual, {});'}
+		),
+		testCase('always', 't.deepEqual({}, actual);',
+			[missingError, outOfOrderError(1, 13, 1, 23)],
+			{output: 't.deepEqual(actual, {});'}
+		),
+		testCase('never', 't.deepEqual({}, actual, \'message\');',
+			[foundError, outOfOrderError(1, 13, 1, 23)],
+			{output: 't.deepEqual(actual, {}, \'message\');'}
+		),
+		testCase(false, 't.deepEqual({}, actual, extra, \'message\');',
+			tooManyError(3)
+		),
+		testCase(false, 't.deepEqual({}, (actual));',
+			outOfOrderError(1, 13, 1, 25),
+			{output: 't.deepEqual((actual), {});'}
+		),
+		testCase(false, 't.deepEqual({}, actual/*: type */);',
+			outOfOrderError(1, 13, 1, 34)
+		),
+		testCase(
+			false,
+			`t.deepEqual(// Line comment 1
+				'static' // Line Comment 2
+				, // Line Comment 3
+				dynamic // Line Comment 4
+				// Line Comment 5
+			); // Line Comment 6`,
+			outOfOrderError(1, 13, 5, 22)
+		),
+		testCase(false, 't.assert(\'static\' !== dynamic);',
+			outOfOrderError(1, 10, 1, 30),
+			{output: 't.assert(dynamic !== \'static\');'}
+		),
+		testCase(false, 't.true(\'static\' <= dynamic);',
+			outOfOrderError(1, 8, 1, 27),
+			{output: 't.true(dynamic >= \'static\');'}
+		),
+		testCase(false, 't.false(\'static\' < dynamic);',
+			outOfOrderError(1, 9, 1, 27),
+			{output: 't.false(dynamic > \'static\');'}
+		),
+		testCase(false, 't.truthy(\'static\' > dynamic);',
+			outOfOrderError(1, 10, 1, 28),
+			{output: 't.truthy(dynamic < \'static\');'}
+		),
+		testCase(false, 't.falsy(\'static\' >= dynamic);',
+			outOfOrderError(1, 9, 1, 28),
+			{output: 't.falsy(dynamic <= \'static\');'}
+		),
+		testCase(false, 't.true(\'static\' === actual/*: type */);',
+			outOfOrderError(1, 8, 1, 38)
+		),
+		...statics.map(expression =>
+			testCase(false, `t.deepEqual(${expression}, dynamic);`,
+				outOfOrderError(1, 13, 1, 22 + expression.length),
+				{output: `t.deepEqual(dynamic, ${expression});`}
+			)
+		),
+		...dynamics.map(expression =>
+			testCase(false, `t.deepEqual('static', ${expression});`,
+				outOfOrderError(1, 13, 1, 23 + expression.length),
+				{output: `t.deepEqual(${expression}, 'static');`}
+			)
+		)
 	]
 });