diff --git a/docs/reference/advanced.md b/docs/reference/advanced.md index 2b3435c..e37698d 100644 --- a/docs/reference/advanced.md +++ b/docs/reference/advanced.md @@ -172,3 +172,9 @@ a given function or module. Enable with `EQWALIZER_CHECK_REDUNDANT_GUARDS=true`. With this setting, eqWAlizer will attempt to detect and report redundant type assertions. See [redundant_guard error](./errors.md#redundant_guard). + +### Spec coverage of function clauses + +Enable checks of proper coverage of function clauses by specs using `EQWALIZER_CLAUSE_COVERAGE=true`. +By default, eqWAlizer will not check coverage of function clauses by the corresponding spec, +hence a clause may be implicitly left unchecked. diff --git a/eqwalizer/src/main/resources/application.conf b/eqwalizer/src/main/resources/application.conf index 02ae428..5a893d1 100644 --- a/eqwalizer/src/main/resources/application.conf +++ b/eqwalizer/src/main/resources/application.conf @@ -17,4 +17,6 @@ eqwalizer { mode = ${?EQWALIZER_MODE} error_depth = 4 error_depth = ${?EQWALIZER_ERROR_DEPTH} + clause_coverage = false + clause_coverage = ${?EQWALIZER_CLAUSE_COVERAGE} } diff --git a/eqwalizer/src/main/scala/com/whatsapp/eqwalizer/package.scala b/eqwalizer/src/main/scala/com/whatsapp/eqwalizer/package.scala index 1c13e3b..8c1847c 100644 --- a/eqwalizer/src/main/scala/com/whatsapp/eqwalizer/package.scala +++ b/eqwalizer/src/main/scala/com/whatsapp/eqwalizer/package.scala @@ -46,6 +46,7 @@ package object eqwalizer { eqwater: Boolean, tolerateErrors: Boolean, checkRedundantGuards: Boolean, + clauseCoverage: Boolean, mode: Mode.Mode, errorDepth: Int, ) { @@ -76,6 +77,7 @@ package object eqwalizer { eqwater = config.getBoolean("eqwater"), tolerateErrors = config.getBoolean("tolerate_errors"), checkRedundantGuards = config.getBoolean("check_redundant_guards"), + clauseCoverage = config.getBoolean("clause_coverage"), mode, errorDepth = config.getInt("error_depth"), ) diff --git a/eqwalizer/src/main/scala/com/whatsapp/eqwalizer/tc/Check.scala b/eqwalizer/src/main/scala/com/whatsapp/eqwalizer/tc/Check.scala index 968e7fa..80b2b99 100644 --- a/eqwalizer/src/main/scala/com/whatsapp/eqwalizer/tc/Check.scala +++ b/eqwalizer/src/main/scala/com/whatsapp/eqwalizer/tc/Check.scala @@ -35,10 +35,17 @@ final class Check(pipelineContext: PipelineContext) { if (occurrence.eqwater(f.clauses)) { val clauseEnvs = occurrence.clausesEnvs(f.clauses, ft.argTys, Map.empty) f.clauses + .lazyZip(1 to f.clauses.length) .lazyZip(clauseEnvs) - .map((clause, occEnv) => checkClause(clause, argTys, resTy, occEnv, Set.empty)) + .foreach((clause, index, occEnv) => + checkClause(clause, argTys, resTy, occEnv, Set.empty, checkCoverage = (index != f.clauses.length)) + ) } else { - f.clauses.map(checkClause(_, argTys, resTy, Env.empty, Set.empty)) + f.clauses + .lazyZip(1 to f.clauses.length) + .foreach((clause, index) => + checkClause(clause, argTys, resTy, Env.empty, Set.empty, checkCoverage = (index != f.clauses.length)) + ) } } @@ -96,6 +103,7 @@ final class Check(pipelineContext: PipelineContext) { resTy: Type, env0: Env, exportedVars: Set[String], + checkCoverage: Boolean = false, ): Env = { val patVars = Vars.clausePatVars(clause) val env1 = util.enterScope(env0, patVars) @@ -105,6 +113,9 @@ final class Check(pipelineContext: PipelineContext) { typeInfo.setCollect(true) val (_, env3) = elabPat.elabPats(clause.pats, argTys, env2) val env4 = elabGuard.elabGuards(clause.guards, env3) + if (pipelineContext.clauseCoverage && checkCoverage && env4.exists { case (_, ty) => subtype.isNoneType(ty) }) { + diagnosticsInfo.add(ClauseNotCovered(clause.pos)) + } val env5 = checkBody(clause.body, resTy, env4) util.exitScope(env0, env5, exportedVars) } diff --git a/eqwalizer/src/main/scala/com/whatsapp/eqwalizer/tc/TcDiagnostics.scala b/eqwalizer/src/main/scala/com/whatsapp/eqwalizer/tc/TcDiagnostics.scala index d8d8232..e67ed2e 100644 --- a/eqwalizer/src/main/scala/com/whatsapp/eqwalizer/tc/TcDiagnostics.scala +++ b/eqwalizer/src/main/scala/com/whatsapp/eqwalizer/tc/TcDiagnostics.scala @@ -150,6 +150,11 @@ object TcDiagnostics { def errorName = "ambiguous_union" override def erroneousExpr: Option[Expr] = Some(expr) } + case class ClauseNotCovered(pos: Pos) extends TypeError { + override val msg: String = "Clause is not covered by spec" + val errorName = "clause_not_covered" + override val erroneousExpr: Option[Expr] = None + } implicit val codec: JsonValueCodec[TypeError] = JsonCodecMaker.make( CodecMakerConfig.withAllowRecursiveTypes(true).withDiscriminatorFieldName(None).withFieldNameMapper { diff --git a/eqwalizer/src/main/scala/com/whatsapp/eqwalizer/tc/package.scala b/eqwalizer/src/main/scala/com/whatsapp/eqwalizer/tc/package.scala index 25d5ada..fbe444a 100644 --- a/eqwalizer/src/main/scala/com/whatsapp/eqwalizer/tc/package.scala +++ b/eqwalizer/src/main/scala/com/whatsapp/eqwalizer/tc/package.scala @@ -70,5 +70,6 @@ package object tc { new DiagnosticsInfo() val errorDepth: Int = options.errorDepth.getOrElse(config.errorDepth) + val clauseCoverage: Boolean = config.clauseCoverage } } diff --git a/eqwalizer/src/test/resources/application.conf b/eqwalizer/src/test/resources/application.conf index cb2a54f..c7b6829 100644 --- a/eqwalizer/src/test/resources/application.conf +++ b/eqwalizer/src/test/resources/application.conf @@ -13,4 +13,6 @@ eqwalizer { check_redundant_guards = ${?EQWALIZER_CHECK_REDUNDANT_GUARDS} mode = standalone mode = ${?EQWALIZER_MODE} + clause_coverage = false + clause_coverage = ${?EQWALIZER_CLAUSE_COVERAGE} }