Skip to content

feat(eslint-plugin-jest): addvalid-expect rule#1017

Open
eryue0220 wants to merge 7 commits into
web-infra-dev:mainfrom
eryue0220:feat/jest-valid-expect
Open

feat(eslint-plugin-jest): addvalid-expect rule#1017
eryue0220 wants to merge 7 commits into
web-infra-dev:mainfrom
eryue0220:feat/jest-valid-expect

Conversation

@eryue0220
Copy link
Copy Markdown
Contributor

Summary

  • Port valid-expect from eslint-plugin-jest to rslint.

Related Links

Tracking issue: #476
eslint-plugin-jest/valid-expect doc code

Checklist

  • Tests updated (or not required).
  • Documentation updated (or not required).

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements the jest/valid-expect rule, which ensures that expect() calls in Jest tests have the correct number of arguments, are followed by a matcher call, and that async assertions are properly awaited or returned. The implementation includes the rule logic, documentation, and comprehensive test suites in both Go and TypeScript. Feedback was provided regarding the tooManyArgsRange function, where decrementing the end position may result in inaccurate diagnostic highlights in the UI due to how AST node end positions are typically handled.

Comment on lines +281 to +283
if end > start {
end--
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The end-- logic here is likely incorrect. In Go's AST, End() returns the position immediately after the last character of the node (exclusive). By decrementing end, the reported range will exclude the last character of the last extra argument, leading to an inaccurate diagnostic highlight in the UI. Ensure the implementation maintains exact alignment with ESLint's semantics and uses UTF-16 code units for LSP compatibility.

return core.NewTextRange(start, end)
References
  1. For linter rules ported from or inspired by ESLint, maintain exact alignment with ESLint's semantics, even for edge cases where a simpler implementation might seem practically equivalent.
  2. For LSP compatibility, calculate string character positions by counting UTF-16 code units, not bytes or runes. Characters outside the BMP (e.g., emoji) count as two units.

ctx.ReportNode(descriptor.node, msg)
}

func reportUncalledMatcherMember(ctx rule.RuleContext, node *ast.Node) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reportUncalledMatcherMember fires for any member chain whose first segment is literally expect, even when expect itself was never called — upstream only has a CallExpression listener, so a bare member access is never visited.

expect.extend;    → upstream: no error | here: matcherNotCalled
expect.resolves;  → upstream: no error | here: matcherNotFound

Only assertions/hasAssertions are exempted, so every other bare expect.x is a false positive.

return strings.HasPrefix(callee, "Promise.")
}

func findPromiseCallExpressionNode(node *ast.Node) *ast.Node {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findPromiseCallExpressionNode walks every ancestor up to the function boundary; upstream only inspects node.parent (and the grandparent for an ArrayExpression), so this latches onto a Promise.* call upstream would never reach.

Promise.all([ foo(expect(Promise.resolve(2)).resolves.not.toBeDefined()) ])
  upstream: asyncMustBeAwaited on the assertion
  here:     promisesWithAsyncAssertionsMustBeAwaited on Promise.all  (wrong node + messageId)

And await Promise.all([ foo(expect(...)) ]) climbs to the awaited Promise.all and reports nothing, while upstream still flags the un-awaited inner assertion (missed report).

return rule.RuleListeners{
ast.KindCallExpression: func(node *ast.Node) {
entries := utils.GetJestFnMemberEntries(node)
if !isExpectChain(entries, ctx.Settings) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isExpectChain matches the literal name "expect" before ParseJestFnCall runs, so aliased imports are never resolved. Other jest rules (prefer-to-contain, no-standalone-expect) support this alias.

import { expect as pleaseExpect } from '@jest/globals'; pleaseExpect(p).resolves.toBe(2)
  upstream: asyncMustBeAwaited
  here:     nothing (missed report)

}

parsed := utils.ParseJestFnCall(node, ctx)
if parsed == nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here parsed == nil can't distinguish "not jest's expect" from "malformed expect chain": isExpectChain passes on the name, ParseJestFnCall returns nil for a non-jest expect, and this block re-derives a reason and reports anyway.

import { expect } from 'chai'; expect(foo).to.equal(bar)
  upstream: no error (resolveToJestFn returns null for a non-jest expect)
  here:     modifierUnknown (false positive)

This and the isExpectChain gate above would both be fixed by reusing a three-state parse entry (parsed / reason / not-jest-null) like upstream's parseJestFnCallWithReason, instead of a name-gate plus reporting-on-nil.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants