From 1b6ab9bde43263a06701cd46bb46d92c45351426 Mon Sep 17 00:00:00 2001 From: zainab-ali Date: Mon, 3 Nov 2025 16:49:34 +0000 Subject: [PATCH] Add EnforceCluesInExpect rule. --- .../META-INF/services/scalafix.v1.Rule | 1 + .../main/scala/fix/EnforceCluesInExpect.scala | 75 +++++++++++++++ .../scala/fix/EnforceCluesInExpectTest.scala | 91 +++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 scalafix/rules/src/main/scala/fix/EnforceCluesInExpect.scala create mode 100644 scalafix/v0_9_0/input/src/main/scala/fix/EnforceCluesInExpectTest.scala diff --git a/scalafix/rules/src/main/resources/META-INF/services/scalafix.v1.Rule b/scalafix/rules/src/main/resources/META-INF/services/scalafix.v1.Rule index 5c2edbdd..7b458a55 100644 --- a/scalafix/rules/src/main/resources/META-INF/services/scalafix.v1.Rule +++ b/scalafix/rules/src/main/resources/META-INF/services/scalafix.v1.Rule @@ -3,3 +3,4 @@ fix.RenameExpectToExpectSame fix.AddClueToExpect fix.RenameCancelToIgnore fix.RenameAssertionExceptionToExpectationFailed +fix.EnforceCluesInExpect diff --git a/scalafix/rules/src/main/scala/fix/EnforceCluesInExpect.scala b/scalafix/rules/src/main/scala/fix/EnforceCluesInExpect.scala new file mode 100644 index 00000000..f3f66e69 --- /dev/null +++ b/scalafix/rules/src/main/scala/fix/EnforceCluesInExpect.scala @@ -0,0 +1,75 @@ +package fix + +import scalafix.v1._ +import scala.meta._ + +class EnforceCluesInExpect extends SemanticRule("EnforceCluesInExpect") { + + import EnforceCluesInExpect._ + override def fix(implicit doc: SemanticDocument): Patch = { + val expectMethod = + SymbolMatcher.normalized("weaver/Expectations.Helpers#expect.") + val expectAllMethod = SymbolMatcher.normalized("weaver/ExpectMacro#all().") + + doc.tree.collect { + case Term.Apply.After_4_6_0(expectMethod(_), + Term.ArgClause(List(tree), _)) => + enforceCluesOrRename(tree) + case Term.Apply.After_4_6_0(expectAllMethod(_), + Term.ArgClause(trees, _)) => + trees.map(enforceCluesOrRename).asPatch + }.asPatch + } + + private def enforceCluesOrRename(tree: Tree)(implicit + doc: SemanticDocument): Patch = { + if (hasClues(tree)) { + Patch.empty + } else { + tree match { + case q"$lhs == $rhs" => Patch.lint(RenameToExpectSame(tree.pos)) + case q"$lhs === $rhs" => Patch.lint(RenameToExpectEql(tree.pos)) + case _ if cluesAreUseful(tree) => Patch.lint(AddClue(tree.pos)) + case _ => Patch.empty + } + } + } + + private def hasClues(tree: Tree)(implicit doc: SemanticDocument): Boolean = { + val clueSymbol = + SymbolMatcher.normalized("weaver/internals/ClueHelpers#clue().") + tree.collect { + case clueSymbol(_) => () + }.nonEmpty + } + + private def cluesAreUseful(tree: Tree): Boolean = { + tree match { + case Term.Name(_) => + // Clues are not useful for names e.g. `expect(exists)` where `exists` is a boolean value. + false + case Term.ApplyUnary(Term.Name("!"), Term.Name(_)) => + // Clues are not useful for negated names e.g. `expect(!exists)` where `exists` is a boolean value. + false + case _ => true + } + } +} + +object EnforceCluesInExpect { + + case class RenameToExpectSame(position: Position) extends Diagnostic { + def message = + "equality assertion should use `expect.eql` or `expect.same`. Read https://typelevel.org/weaver-test/features/asserting_equality.html for more details." + } + + case class RenameToExpectEql(position: Position) extends Diagnostic { + def message = + "equality assertion should use `expect.eql`. Read https://typelevel.org/weaver-test/features/asserting_equality.html for more details." + } + + case class AddClue(position: Position) extends Diagnostic { + def message = + "assertion must contain clues. Read https://typelevel.org/weaver-test/features/troubleshooting_failures.html#using-clue-with-expect for more details." + } +} diff --git a/scalafix/v0_9_0/input/src/main/scala/fix/EnforceCluesInExpectTest.scala b/scalafix/v0_9_0/input/src/main/scala/fix/EnforceCluesInExpectTest.scala new file mode 100644 index 00000000..4b6b09ff --- /dev/null +++ b/scalafix/v0_9_0/input/src/main/scala/fix/EnforceCluesInExpectTest.scala @@ -0,0 +1,91 @@ +/* +rule = EnforceCluesInExpect + */ +package fix +import weaver._ +import cats.syntax.all._ + +object EnforceCluesInExpect extends SimpleIOSuite { + + val a: Int = 1 + val b: Int = 2 + val c: Int = 3 + + pureTest("expect") { + expect(a < b) // assert: EnforceCluesInExpect + } + + pureTest("expect ==") { + expect(a == b) // assert: EnforceCluesInExpect + } + + pureTest("expect ===") { + expect(a === b) // assert: EnforceCluesInExpect + } + + pureTest("expect message") { + expect(a < b) /* assert: EnforceCluesInExpect + ^^^^^ + assertion must contain clues. Read https://typelevel.org/weaver-test/features/troubleshooting_failures.html#using-clue-with-expect for more details. + */ + } + + pureTest("multiple expect") { + expect(a == b) and expect(b == c) && not(expect(c === a)) // assert: EnforceCluesInExpect + } + + pureTest("message ==") { + expect(a == b) /* assert: EnforceCluesInExpect + ^^^^^^ + equality assertion should use `expect.eql` or `expect.same`. Read https://typelevel.org/weaver-test/features/asserting_equality.html for more details. + */ + } + + pureTest("message ===") { + expect(a === b) /* assert: EnforceCluesInExpect + ^^^^^^^ + equality assertion should use `expect.eql`. Read https://typelevel.org/weaver-test/features/asserting_equality.html for more details. + */ + } + + pureTest("expect with clue in expectation") { + expect(clue(1) > 0) + } + + pureTest("expect with boolean expression") { + val isEqual = a == b + expect(isEqual) + } + + pureTest("expect with negated boolean expression") { + val isEqual = a == b + expect(!isEqual) + } + + pureTest("expect.all") { + expect.all(a == b, b == c) // assert: EnforceCluesInExpect + } + + pureTest("expect.all message") { + expect.all(a > b) /* assert: EnforceCluesInExpect + ^^^^^ + assertion must contain clues. Read https://typelevel.org/weaver-test/features/troubleshooting_failures.html#using-clue-with-expect for more details. + */ + } + + pureTest("expect.all equality message") { + expect.all(a == b) /* assert: EnforceCluesInExpect + ^^^^^^ + equality assertion should use `expect.eql` or `expect.same`. Read https://typelevel.org/weaver-test/features/asserting_equality.html for more details. + */ + } + + + pureTest("expect.all with clues in some expectations") { + expect.all(a == clue(b), b == c) // assert: EnforceCluesInExpect + } + + pureTest("expect.all with clues in every expectation") { + expect.all(a == clue(b), b == clue(c)) + } +}