Skip to content

Add negated-assert checker and extend require-error: detect and fix if !assert.Xxx { return } patterns - #297

Open
mmorel-35 wants to merge 1 commit into
Antonboom:masterfrom
mmorel-35:negated-assert
Open

Add negated-assert checker and extend require-error: detect and fix if !assert.Xxx { return } patterns#297
mmorel-35 wants to merge 1 commit into
Antonboom:masterfrom
mmorel-35:negated-assert

Conversation

@mmorel-35

@mmorel-35 mmorel-35 commented Mar 15, 2026

Copy link
Copy Markdown
Contributor

The require-error checker missed !assert.xxx() in if conditions because *ast.UnaryExpr was not handled in the inIfCond detection stack walk. This caused the "last assertion" skip logic to produce inconsistent results. Additionally, the if !assert.Xxx { return } pattern is a general anti-pattern applicable to any assertion, not just error ones — so a dedicated checker was created to cover it broadly. require-error was also extended to independently detect and fix this pattern for error assertions, so it works correctly even when used as the only enabled checker.

Changes

Bug fix — *ast.UnaryExpr detection in if conditions (require_error.go)

  • Extends the stack inspection to recognise !assert.xxx() (stack: IfStmt → UnaryExpr → CallExpr) and !assert.xxx() || … (stack: IfStmt → BinaryExpr → UnaryExpr → CallExpr) as inIfCond, ensuring require-error correctly skips these patterns without false positives.
  • ParenExpr nodes are treated as transparent throughout the stack walk and the collect functions, so if (!assert.NoError(...)) { return } and if !(assert.NoError(...)) { return } are also recognised.

New dedicated checker — negated-assert (internal/checkers/negated_assert.go)

Detects and provides a SuggestedFix for any negated assertion in an if condition guarding a return or continue:

// before
if !assert.NoError(t, err) {
    return
}
if !assert.Equal(t, expected, actual) {
    return
}

// after fix
require.NoError(t, err)
require.Equal(t, expected, actual)

And the compound || variant:

// before
if !assert.NoError(t, err) || !assert.Equal(t, expected, actual) {
    return
}

// after fix
require.NoError(t, err)
require.Equal(t, expected, actual)

A pattern is only fixed when all top-level conditions are negated testify assert package calls joined by ||, the body is a single return/continue, there is no else, the if has no init clause, and the if is not an else if branch (which would leave a dangling else). Patterns using &&, with complex bodies, with an init clause, or inside goroutines/HTTP handlers/test cleanup are skipped. The require import is added automatically if absent.

Extended require-error — negated error-assertion if-patterns

require-error now independently detects and fixes if !assert.ErrorXxx { return/continue } patterns for error assertions, so the pattern is always caught regardless of whether negated-assert is also enabled:

// before
if !assert.NoError(t, err1) || !assert.NoError(t, err2) {
    return
}

// after
require.NoError(t, err1)
require.NoError(t, err2)

The scope split is:

  • require-error handles error assertions (NoError, Error, ErrorIs, ErrorAs, EqualError, ErrorContains, NotErrorIs) in negated-if patterns — within its existing scope.
  • negated-assert handles all assertions (including non-error ones) in negated-if patterns.

When both checkers are enabled, both independently fire on error assertion negated-if patterns.

Robustness fixes

  • Init clause guard: both negated-assert and require-error Stage 2a now skip if statements with an init clause (e.g. if err := foo(); !assert.NoError(t, err) { return }) to avoid dropping the init statement and breaking compilation.
  • Dot-import handling: both fix builders correctly handle the case where require is dot-imported (empty qualifier returned by addImportFix), producing valid call text and fix messages.
  • ParenExpr unwrapping: collectNegatedAssertOrCalls and collectRequireErrorNegatedOrCalls unwrap *ast.ParenExpr so parenthesised conditions are recognised as fixable patterns.
  • Stack-walk callIsNegatedInIfCond: replaces the earlier piecemeal pattern matching; treats ParenExpr and any BinaryExpr as transparent, correctly classifying && patterns as inIfCond (skipped, not falsely reported) while still restricting fixable patterns to ||-only chains in Stage 2a.

Import helper (helpers_import.go)

New file providing addImportFix() — returns the local qualifier for a package and an optional TextEdit to add the import — ported from the http-const branch to avoid merge conflicts.

Registry / testgen / README

  • negated-assert registered as an AdvancedChecker, enabled by default, placed between go-require and require-error.
  • internal/testgen/gen_negated_assert.go added with ErroredTemplate and GoldenTemplate.
  • Test data generated: checkers-default/negated-assert/negated_assert_test.go + .golden.
  • negated-assert-skip-logic/ not-default test directory added, covering goroutine, HTTP handler, cleanup, compound ||, and continue patterns.
  • require-error-skip-logic/negated_if_test.go updated with // want comments for error assertion negated-if patterns (and .golden file added); compound || test uses two independent error variables to demonstrate a logically sound pattern.
  • README.md updated: require-error section shows negated-if examples and partial autofix (🤏); negated-assert section added; cross-references between both checkers.

Closes #125

Copilot AI added a commit to mmorel-35/testifylint that referenced this pull request May 28, 2026
mmorel-35 pushed a commit to mmorel-35/testifylint that referenced this pull request May 28, 2026
@Antonboom Antonboom added the llm-based LLM shit label Jun 22, 2026
@mmorel-35
mmorel-35 force-pushed the negated-assert branch 2 times, most recently from abaf8d7 to 7799b1e Compare August 7, 2026 12:01
…ix `if !assert.Xxx { return }` patterns

Signed-off-by: Matthieu MOREL <matthieu.morel35@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

llm-based LLM shit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

require-error: detect if !assert.NoError

3 participants