diff --git a/scalafix-core/shared/src/main/scala/scalafix/internal/config/DisableSyntaxConfig.scala b/scalafix-core/shared/src/main/scala/scalafix/internal/config/DisableSyntaxConfig.scala index 51da1eed6..c5feb2e28 100644 --- a/scalafix-core/shared/src/main/scala/scalafix/internal/config/DisableSyntaxConfig.scala +++ b/scalafix-core/shared/src/main/scala/scalafix/internal/config/DisableSyntaxConfig.scala @@ -6,11 +6,14 @@ import MetaconfigPendingUpstream.XtensionConfScalafix import scala.meta.tokens.Token +import java.util.regex.{Pattern, PatternSyntaxException} + case class DisableSyntaxConfig( keywords: Set[DisabledKeyword] = Set(), noSemicolons: Boolean = false, noTabs: Boolean = false, - noXml: Boolean = false + noXml: Boolean = false, + regex: List[CustomMessage[Pattern]] = Nil ) { implicit val reader: ConfDecoder[DisableSyntaxConfig] = ConfDecoder.instanceF[DisableSyntaxConfig]( @@ -19,12 +22,26 @@ case class DisableSyntaxConfig( c.getField(keywords) |@| c.getField(noSemicolons) |@| c.getField(noTabs) |@| - c.getField(noXml) + c.getField(noXml) |@| + c.getField(regex) ).map { - case (((a, b), c), d) => - DisableSyntaxConfig(a, b, c, d) + case ((((a, b), c), d), e) => + DisableSyntaxConfig(a, b, c, d, e) }) + implicit val patternReader: ConfDecoder[Pattern] = { + ConfDecoder.stringConfDecoder.flatMap(pattern => + try { + Configured.Ok(Pattern.compile(pattern, Pattern.MULTILINE)) + } catch { + case ex: PatternSyntaxException => + Configured.NotOk(ConfError.msg(ex.getMessage)) + }) + } + + implicit val customMessageReader: ConfDecoder[CustomMessage[Pattern]] = + CustomMessage.decoder(field = "pattern") + def isDisabled(keyword: String): Boolean = keywords.contains(DisabledKeyword(keyword)) } diff --git a/scalafix-core/shared/src/main/scala/scalafix/internal/rule/DisableSyntax.scala b/scalafix-core/shared/src/main/scala/scalafix/internal/rule/DisableSyntax.scala index 1ec09e50f..3893dea69 100644 --- a/scalafix-core/shared/src/main/scala/scalafix/internal/rule/DisableSyntax.scala +++ b/scalafix-core/shared/src/main/scala/scalafix/internal/rule/DisableSyntax.scala @@ -1,11 +1,13 @@ package scalafix.internal.rule import scala.meta._ +import scala.meta.tokens.Token.Comment import metaconfig.{Conf, Configured} import scalafix.rule.{Rule, RuleCtx} import scalafix.lint.LintMessage import scalafix.lint.LintCategory import scalafix.util.SymbolMatcher +import scalafix.internal.util.IntervalSet import scalafix.internal.config.{DisableSyntaxConfig, Keyword} import scalafix.syntax._ @@ -23,16 +25,35 @@ final case class DisableSyntax( .map(DisableSyntax(_)) override def check(ctx: RuleCtx): Seq[LintMessage] = { - ctx.tree.tokens.collect { - case token @ Keyword(keyword) if config.isDisabled(keyword) => - errorCategory.copy(id = s"keywords.$keyword").at(s"$keyword is disabled", token.pos) - case token @ Token.Semicolon() if config.noSemicolons => - error("noSemicolons", token) - case token @ Token.Tab() if config.noTabs => - error("noTabs", token) - case token @ Token.Xml.Start() if config.noXml => - error("noXml", token) - }.toSeq + def pos(offset: Int): Position = + Position.Range(ctx.input, offset, offset) + val regexLintMessages = Seq.newBuilder[LintMessage] + config.regex.foreach { regex => + val matcher = regex.value.matcher(ctx.input.chars) + val pattern = regex.value.pattern + val message = regex.message.getOrElse(s"$pattern is disabled") + while (matcher.find()) { + regexLintMessages += + errorCategory + .copy(id = pattern) + .at(message, pos(matcher.start)) + } + } + val tokensLintMessage = + ctx.tree.tokens.collect { + case token @ Keyword(keyword) if config.isDisabled(keyword) => + errorCategory + .copy(id = s"keywords.$keyword") + .at(s"$keyword is disabled", token.pos) + case token @ Token.Semicolon() if config.noSemicolons => + error("noSemicolons", token) + case token @ Token.Tab() if config.noTabs => + error("noTabs", token) + case token @ Token.Xml.Start() if config.noXml => + error("noXml", token) + }.toSeq + + tokensLintMessage ++ regexLintMessages.result() } private val errorCategory: LintCategory = diff --git a/scalafix-core/shared/src/main/scala/scalafix/internal/util/IntervalSet.scala b/scalafix-core/shared/src/main/scala/scalafix/internal/util/IntervalSet.scala index 1b9fc37a5..97a960633 100644 --- a/scalafix-core/shared/src/main/scala/scalafix/internal/util/IntervalSet.scala +++ b/scalafix-core/shared/src/main/scala/scalafix/internal/util/IntervalSet.scala @@ -12,14 +12,14 @@ class IntervalSet(range: BitSet) { } override def toString: String = { - val intervals = - if(range.isEmpty) Nil + val intervals = + if (range.isEmpty) Nil else { var cur = range.head var start = cur val interval = List.newBuilder[(Int, Int)] - range.tail.foreach{bit => - if(cur + 1 != bit) { + range.tail.foreach { bit => + if (cur + 1 != bit) { interval += ((start, cur)) start = bit } @@ -29,8 +29,9 @@ class IntervalSet(range: BitSet) { interval.result() } - val is = intervals.map{ case (start, end) => - s"[$start, $end]" + val is = intervals.map { + case (start, end) => + s"[$start, $end]" } s"""IntervalSet(${is.mkString(", ")})""" diff --git a/scalafix-tests/input/src/main/scala/test/DisableSyntax.scala b/scalafix-tests/input/src/main/scala/test/DisableSyntax.scala index 2c4d27584..ac7976098 100644 --- a/scalafix-tests/input/src/main/scala/test/DisableSyntax.scala +++ b/scalafix-tests/input/src/main/scala/test/DisableSyntax.scala @@ -1,4 +1,4 @@ -/* ONLY +/* rules = DisableSyntax DisableSyntax.keywords = [ var @@ -9,11 +9,19 @@ DisableSyntax.keywords = [ DisableSyntax.noTabs = true DisableSyntax.noSemicolons = true DisableSyntax.noXml = true +DisableSyntax.regex = [ + { + pattern = "[P|p]imp" + message = "Please consider a less offensive word such as Extension" + } + "Await\\.result" +] */ package test import scala.concurrent._ import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global case object DisableSyntax { @@ -31,10 +39,10 @@ case object DisableSyntax { xml // assert: DisableSyntax.noXml // assert: DisableSyntax.noTabs - // implicit class StringPimp(value: String) { // assert: DisableSyntax.regex.pimp - // def -(other: String): String = s"$value - $other" - // } + implicit class StringPimp(value: String) { // assert: DisableSyntax.[P|p]imp + def -(other: String): String = s"$value - $other" + } - // // actually 7.5 million years - // Await.result(Future(42), 75.days) // assert: DisableSyntax.regex.Await.result + // actually 7.5 million years + Await.result(Future(42), 75.days) // assert: DisableSyntax.Await\.result } diff --git a/scalafix-tests/unit/src/test/scala/scalafix/tests/cli/CliGitDiffTests.scala b/scalafix-tests/unit/src/test/scala/scalafix/tests/cli/CliGitDiffTests.scala index c610f46a1..527b0ccd0 100644 --- a/scalafix-tests/unit/src/test/scala/scalafix/tests/cli/CliGitDiffTests.scala +++ b/scalafix-tests/unit/src/test/scala/scalafix/tests/cli/CliGitDiffTests.scala @@ -49,7 +49,7 @@ class CliGitDiffTests() extends FunSuite with DiffAssertions { val expected = s"""|Running DisableSyntax - |$newCodeAbsPath:3:3: error: [DisableSyntax.keywords.var] keywords.var is disabled + |$newCodeAbsPath:3:3: error: [DisableSyntax.keywords.var] var is disabled | var newVar = 1 | ^ |""".stripMargin @@ -90,7 +90,7 @@ class CliGitDiffTests() extends FunSuite with DiffAssertions { val expected = s"""|Running DisableSyntax - |$oldCodeAbsPath:7:3: error: [DisableSyntax.keywords.var] keywords.var is disabled + |$oldCodeAbsPath:7:3: error: [DisableSyntax.keywords.var] var is disabled | var newVar = 2 | ^ |""".stripMargin @@ -132,7 +132,7 @@ class CliGitDiffTests() extends FunSuite with DiffAssertions { val expected = s"""|Running DisableSyntax - |$newCodeAbsPath:5:3: error: [DisableSyntax.keywords.var] keywords.var is disabled + |$newCodeAbsPath:5:3: error: [DisableSyntax.keywords.var] var is disabled | var newVar = 2 | ^ |""".stripMargin @@ -190,7 +190,7 @@ class CliGitDiffTests() extends FunSuite with DiffAssertions { val expected = s"""|Running DisableSyntax - |$newCodeAbsPath:5:3: error: [DisableSyntax.keywords.var] keywords.var is disabled + |$newCodeAbsPath:5:3: error: [DisableSyntax.keywords.var] var is disabled | var newVar = 2 | ^ |""".stripMargin