Skip to content

Implement S8967: inline snapshots should not contain interpolations#7407

Merged
nathsou merged 10 commits into
masterfrom
new-rule/S8967
Jul 6, 2026
Merged

Implement S8967: inline snapshots should not contain interpolations#7407
nathsou merged 10 commits into
masterfrom
new-rule/S8967

Conversation

@nathsou

@nathsou nathsou commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • implement S8967 for inline snapshot template interpolations in Jest and Vitest
  • keep the detection narrow to built-in inline snapshot matchers rooted in expect(...), including not, resolves, and rejects
  • add focused unit tests and sync the generated rule metadata/profile data

Links


Summary by Gitar

  • New rule implementation:
    • Implemented S8967 to detect template interpolations in Jest and Vitest inline snapshots.
  • Shared logic extraction:
    • Extracted chain-walking logic into a new helper, expect-call-chain.ts, to support S8967 and refactor S8780.
  • Refactoring:
    • Updated S8780 to utilize the new collectCallChain and getRootCall helpers, improving handling of computed member access in expectation chains.
  • Testing and metadata:
    • Added comprehensive unit tests for S8967 and updated Sonar_way_profile.json to include the new rule.

This will update automatically on new commits.

@nathsou nathsou self-assigned this Jun 30, 2026
@nathsou
nathsou requested a review from guillemsarda July 3, 2026 11:46
Comment thread packages/analysis/src/jsts/rules/helpers/expect-call-chain.ts

@guillemsarda guillemsarda left a comment

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.

No big changes requested

Comment thread packages/analysis/src/jsts/rules/S8960/rule.ts
Comment thread packages/analysis/src/jsts/rules/S8967/rule.ts Outdated
nathsou added 8 commits July 6, 2026 15:39
…ng the computed-member guard to collectCallChain
… one per interpolation

Reduces noise on snapshots with multiple interpolations, per review feedback.
- Replace the isUnresolved-based fallback in isExpectCall with a
  narrower isGlobalExpect check (no variable, or a variable with no
  declarations) to avoid misclassifying a shadowed local `expect`
  (e.g. a function parameter) as the framework global.
- Fix collectCallChain's JSDoc: segments are collected furthest from
  the root call first, not "innermost first".
@nathsou
nathsou requested a review from guillemsarda July 6, 2026 13:49
@datadog-sonarsource

This comment has been minimized.

@guillemsarda guillemsarda left a comment

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.

Image

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Ruling Report

New issues flagged (4 issues)

S8967

vitest/test/browser/fixtures/expect-dom/toMatchScreenshot.test.ts:134

   132 |       expect(ratio).toMatch(/[01]\.\d{2}/)
   133 | 
>  134 |       expect(errorMessage).toMatchInlineSnapshot(`
   135 |         expect(element).toMatchScreenshot()
   136 | 

vitest/test/browser/fixtures/expect-dom/toMatchScreenshot.test.ts:182

   180 |       })
   181 | 
>  182 |       expect(errorMessage).toMatchInlineSnapshot(`
   183 |         expect(element).toMatchScreenshot()
   184 | 

vitest/test/coverage-test/test/mixed-versions-warning.unit.test.ts:25

    23 |   const message = warn.mock.calls[0][0]
    24 | 
>   25 |   expect(stripVTControlCharacters(message)).toMatchInlineSnapshot(`
    26 |     "Loaded  vitest@1.0.0  and  @vitest/coverage-v8@${version} .
    27 |     Running mixed versions is not supported and may lead into bugs

vitest/test/coverage-test/test/mixed-versions-warning.unit.test.ts:47

    45 |   const message = warn.mock.calls[0][0]
    46 | 
>   47 |   expect(stripVTControlCharacters(message)).toMatchInlineSnapshot(`
    48 |     "Loaded  vitest@1.0.0  and  @vitest/coverage-istanbul@${version} .
    49 |     Running mixed versions is not supported and may lead into bugs

The new rule flags 4 genuine toMatchInlineSnapshot() interpolations in
the vitest project (screenshot error messages and version warnings).
@sonarqube-next

sonarqube-next Bot commented Jul 6, 2026

Copy link
Copy Markdown

@nathsou
nathsou merged commit 845e5ea into master Jul 6, 2026
43 checks passed
@nathsou
nathsou deleted the new-rule/S8967 branch July 6, 2026 15:25
@gitar-bot

gitar-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 2 resolved / 2 findings

Implements rule S8967 to detect template interpolations in inline snapshots and refactors expectation chain logic into shared helpers. Resolved findings regarding documentation clarity in collectCallChain and Playwright async assertion suppression.

✅ 2 resolved
Quality: collectCallChain doc says "innermost first" but returns outermost first

📄 packages/analysis/src/jsts/rules/helpers/expect-call-chain.ts:32-35
The JSDoc for collectCallChain states the segments are collected "innermost first", but the accompanying example and the actual implementation return them outermost-matcher first. For expect(x).not.resolves.toBe(y) the function returns [{toBe}, {resolves}, {not}] — i.e. toBe (the outermost matcher, farthest from the expect(...) root) is first and not (closest/innermost to the root) is last. This is exactly what S8967 relies on (chain[0] must be the inline-snapshot matcher, chain.slice(1) the modifiers), so the code is correct; only the wording is wrong.

Because this is a shared helper consumed by both S8780 and S8967, the mislabeled ordering could lead a future caller to index segments from the wrong end. Suggest updating the comment to say "outermost (matcher) first" to match the behavior.

Edge Case: done-callback guard also suppresses Playwright async assertions

📄 packages/analysis/src/jsts/rules/S8780/rule.ts:120-133 📄 packages/analysis/src/jsts/rules/S8780/rule.ts:175-185 📄 packages/analysis/src/jsts/rules/S8780/rule.ts:309-312
The new usesDoneCallback guard is applied unconditionally at the top of the CallExpression:exit handler, so it runs for every framework — including Playwright. The doc comment justifies this by asserting that "Playwright's fixtures parameter is always a destructuring pattern, never a plain identifier". That is not guaranteed: Playwright's first fixtures argument can legally be a plain identifier, e.g. test('x', async (fixtures) => { await expect(fixtures.page).toBeVisible(); }). In that case usesDoneCallback returns true (first param is an Identifier) and the whole test body is skipped, so a genuinely missing await/return on a Playwright async matcher (getPlaywrightAsyncNode) is silently not reported — a false negative. Playwright never uses a done callback at all, so the guard is conceptually out of scope for it.

Suggested fix: only apply the done-callback guard to the frameworks that actually use it (jest/vitest/jasmine), leaving Playwright detection unaffected. No existing test covers the non-destructured Playwright fixtures parameter, so consider adding one.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants