Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rule.apply: List[Patch] #368

Closed
olafurpg opened this issue Sep 21, 2017 · 6 comments
Closed

Rule.apply: List[Patch] #368

olafurpg opened this issue Sep 21, 2017 · 6 comments
Assignees

Comments

@olafurpg
Copy link
Contributor

olafurpg commented Sep 21, 2017

Following a conversation with @xeno-by from his experience with the scalafix api. This is a proposal to replace Rule.fix/check with a single method Rule.apply(ctx: RuleCtx): List[Fix] where (roughly)

trait Fix {
  def message: LintMessage // LintMessage.empty by default
  def patch: Patch // Patch.empty by default
}

This change addresses several problems with the current design

  • escape hatches Escape hatch / disable linter messages over regions #241, a single Patch can contain a sequence of ctx.addLeft/addRight/remove edits that need atomically trigger or not. See example below, if Baz is inserted inside foo(...) along with an import at the top of a file, then that import should not be inserted since scalafix is disabled around foo(...).
// import a.Baz to be inserted by scalafix
object a {
  // scalafix: off
  foo(...) // Baz to be inserted here by scalafix
  // scalafix: on
}
  • one signature makes it easier to convert a linter into a rewrite (autofix) or vice-versa turn a rewrite into a linter
  • avoids the need for sprinkling .asPatch in most cases.

I think it's possible to introduce this change while keeping compatibility with the current design, and we deprecate Rule.fix/check.

@gabro
Copy link
Collaborator

gabro commented Sep 21, 2017

+1 to the overall design, having a single "action" that returns a rich data structure feels more natural.

Now, for the bikeshedding:

  • why not List[LintMessage] (default: Nil)?

  • avoids the need for sprinkling .asPatch in most cases.

    how?

@MasseGuillaume
Copy link
Contributor

I'm on this.

@gabro
Copy link
Collaborator

gabro commented Dec 12, 2017

I was thinking about this yesterday. That's great @MasseGuillaume!

I think will make it easier to start treating the current fixes as lint rules too. For instance: UnusedImports can report an unused import (in the editor 😇 ) and then the user can decide to fix it (in bulk with scalafix or manually).

@MasseGuillaume
Copy link
Contributor

Yup, we discovered something call CodeLens in lsp where you can attach an action such as remove import. This way you could just do it on one item.

@gabro
Copy link
Collaborator

gabro commented Dec 12, 2017

Absolutely, CodeLens is exactly what I was implying we'd use in the future :)

@MasseGuillaume MasseGuillaume changed the title Rule.apply: List[Fix] Rule.apply: List[Patch] Dec 12, 2017
MasseGuillaume added a commit to MasseGuillaume/scalafix that referenced this issue Dec 13, 2017
@olafurpg
Copy link
Contributor Author

Pending PR to fix this in #503

We went with a design that does not require any changes to the existing API. There is a new method .atomic: Patch to notify scalafix that the patch must be atomically applied. If there is a // scalafix: off Rule comment that affects a position that is touched by the Patch then the whole patch is ignored.

To simplify the Rule API, I think we should consider deprecating check since it's possible to implement it with fix by simply wrapping all ctx.lint(LintMessage) and .asPatch. This is what scalafix does behind the scenes anyways. We can discuss this in a separate PR.

I think will make it easier to start treating the current fixes as lint rules too. For instance: UnusedImports can report an unused import (in the editor 😇 ) and then the user can decide to fix it (in bulk with scalafix or manually).

IntelliJ has this option to run all fixes of a given category in the open file. I use this quite frequently to fix things like repeated missing () for effectful unary methods.

MasseGuillaume added a commit to MasseGuillaume/scalafix that referenced this issue Dec 13, 2017
MasseGuillaume added a commit to MasseGuillaume/scalafix that referenced this issue Dec 13, 2017
MasseGuillaume added a commit to MasseGuillaume/scalafix that referenced this issue Dec 14, 2017
MasseGuillaume added a commit to MasseGuillaume/scalafix that referenced this issue Dec 15, 2017
MasseGuillaume added a commit to MasseGuillaume/scalafix that referenced this issue Dec 15, 2017
I found one counter example in RemoveXmlLiterals:

```
// scalafix-tests/input/src/main/scala/test/RemoveXmlLiteralsAtomic.scala
/*
rules = RemoveXmlLiterals
 */
package test

class RemoveXmlLiteralsAtomic {
  <div></div>// scalafix:ok RemoveXmlLiterals
}

// scalafix-tests/output/src/main/scala/test/RemoveXmlLiteralsAtomic.scala
package test
import scala.xml.quote._ // <<< fail
class RemoveXmlLiteralsAtomic {
  <div></div>// scalafix:ok RemoveXmlLiterals
}
```

This happens because of:

````
if (patch.nonEmpty) patch + importXmlQuote
else patch
```

At this point, patch is not empty but it will be removed subsequently by the escape hatch.

The solution would be to add a `dependsOn` on Patch

```
case class DependsOn(parent: Patch, child: Patch) extends Patch

class Patch {
  // ...
  def dependsOn(other: Patch): Patch = DependsOn(this, other)
}

object Patch {
  private def foreach
    // ...
      case DependsOn(parent, child) =>
        loop(parent)
        loop(child)
    // ...
  }
}

class EscapeHatch {
  def filter
    // ...
    case DependsOn(parent, child) =>
      val childs = loop(name, child)

      if (childs == EmptyPatch) EmptyPatch
      else loop(name, Concat(parent, childs))
}
```
MasseGuillaume added a commit to MasseGuillaume/scalafix that referenced this issue Dec 15, 2017
I found one counter example in RemoveXmlLiterals:

```
// scalafix-tests/input/src/main/scala/test/RemoveXmlLiteralsAtomic.scala
/*
rules = RemoveXmlLiterals
 */
package test

class RemoveXmlLiteralsAtomic {
  <div></div>// scalafix:ok RemoveXmlLiterals
}

// scalafix-tests/output/src/main/scala/test/RemoveXmlLiteralsAtomic.scala
package test
import scala.xml.quote._ // <<< fail
class RemoveXmlLiteralsAtomic {
  <div></div>// scalafix:ok RemoveXmlLiterals
}
```

This happens because of:

````
if (patch.nonEmpty) patch + importXmlQuote
else patch
```

At this point, patch is not empty but it will be removed subsequently by the escape hatch.

The solution would be to add a `dependsOn` on Patch

```
case class DependsOn(parent: Patch, child: Patch) extends Patch

class Patch {
  // ...
  def dependsOn(other: Patch): Patch = DependsOn(this, other)
}

object Patch {
  private def foreach
    // ...
      case DependsOn(parent, child) =>
        loop(parent)
        loop(child)
    // ...
  }
}

class EscapeHatch {
  def filter
    // ...
    case DependsOn(parent, child) =>
      val childs = loop(name, child)

      if (childs == EmptyPatch) EmptyPatch
      else loop(name, Concat(parent, childs))
}
```
MasseGuillaume added a commit to MasseGuillaume/scalafix that referenced this issue Dec 15, 2017
I found one counter example in RemoveXmlLiterals:

```
// scalafix-tests/input/src/main/scala/test/RemoveXmlLiteralsAtomic.scala
/*
rules = RemoveXmlLiterals
 */
package test

class RemoveXmlLiteralsAtomic {
  <div></div>// scalafix:ok RemoveXmlLiterals
}

// scalafix-tests/output/src/main/scala/test/RemoveXmlLiteralsAtomic.scala
package test
import scala.xml.quote._ // <<< fail
class RemoveXmlLiteralsAtomic {
  <div></div>// scalafix:ok RemoveXmlLiterals
}
```

This happens because of:

````
if (patch.nonEmpty) patch + importXmlQuote
else patch
```

At this point, patch is not empty but it will be removed subsequently by the escape hatch.

The solution would be to add a `dependsOn` on Patch

```
case class DependsOn(parent: Patch, child: Patch) extends Patch

class Patch {
  // ...
  def dependsOn(other: Patch): Patch = DependsOn(this, other)
}

object Patch {
  private def foreach
    // ...
      case DependsOn(parent, child) =>
        loop(parent)
        loop(child)
    // ...
  }
}

class EscapeHatch {
  def filter
    // ...
    case DependsOn(parent, child) =>
      val childs = loop(name, child)

      if (childs == EmptyPatch) EmptyPatch
      else loop(name, Concat(parent, childs))
}
```
MasseGuillaume added a commit that referenced this issue Dec 15, 2017
MasseGuillaume added a commit to MasseGuillaume/scalafix that referenced this issue Dec 20, 2017
```
> scalafix --diff-base <tab>
HEAD
ag/gh-pages
ag/issue-260
ag/issue-319
ag/master
...
v0.5.5
v0.5.6
|00| 51abefa -- wip improve sbt completion (13 hours ago)
|01| 3f63b5b -- Merge pull request scalacenter#474 from marcelocenerine/trim_includeNewLine (13 hours ago)
|02| 47b94c0 -- Add leadingSpaces/trailingSpaces methods to TokenList (13 hours ago)
|03| 8bd026d -- Merge pull request scalacenter#503 from MasseGuillaume/feature/368-escape-patches (13 hours ago)
|04| 20e445f -- Escape hatch on Patch (fix scalacenter#368) (13 hours ago)
|05| a2c5d70 -- Merge pull request scalacenter#500 from MasseGuillaume/feature/495-custom-id (13 hours ago)
|06| 59efe7d -- Add CustomMessage to the public api (13 hours ago)
|07| 9ae6071 -- Add id for CustomMessage (fix scalacenter#495) (13 hours ago)
|08| e4a5c35 -- Merge pull request scalacenter#494 from MasseGuillaume/disable-regex (13 hours ago)
|09| a422860 -- Merge pull request scalacenter#497 from olafurpg/disable-signatures (13 hours ago)
|10| 7930947 -- DisableSyntax add regex (13 hours ago)
|11| 5dbdd6b -- IntervalSet test for empty and add toString (13 hours ago)
|12| b022fbd -- DisableSyntax don't repeat DisableSyntax.keyword in message (13 hours ago)
|13| a992b02 -- Assert instead of scalafix:ok (13 hours ago)
|14| 7896ccd -- Refactor Disable to use views. (13 hours ago)
|15| 58acdbe -- Fix scalacenter#493, handle synthetics and symbol signatures in Disable. (13 hours ago)
|16| b48d7f0 -- Merge pull request scalacenter#490 from olafurpg/unmanagedSources (13 hours ago)
|17| e9b2b0a -- s/canFormat/canFix/ (13 hours ago)
|18| 26be6fa -- Use unmanagedSources instead of unmanagedSourceDirectories. (13 hours ago)
|19| 4d46001 -- Merge pull request scalacenter#488 from olafurpg/master (13 hours ago)
```
MasseGuillaume added a commit to MasseGuillaume/scalafix that referenced this issue Dec 22, 2017
```
> scalafix --diff-base <tab>
HEAD
ag/gh-pages
ag/issue-260
ag/issue-319
ag/master
...
v0.5.5
v0.5.6
|00| 51abefa -- wip improve sbt completion (13 hours ago)
|01| 3f63b5b -- Merge pull request scalacenter#474 from marcelocenerine/trim_includeNewLine (13 hours ago)
|02| 47b94c0 -- Add leadingSpaces/trailingSpaces methods to TokenList (13 hours ago)
|03| 8bd026d -- Merge pull request scalacenter#503 from MasseGuillaume/feature/368-escape-patches (13 hours ago)
|04| 20e445f -- Escape hatch on Patch (fix scalacenter#368) (13 hours ago)
|05| a2c5d70 -- Merge pull request scalacenter#500 from MasseGuillaume/feature/495-custom-id (13 hours ago)
|06| 59efe7d -- Add CustomMessage to the public api (13 hours ago)
|07| 9ae6071 -- Add id for CustomMessage (fix scalacenter#495) (13 hours ago)
|08| e4a5c35 -- Merge pull request scalacenter#494 from MasseGuillaume/disable-regex (13 hours ago)
|09| a422860 -- Merge pull request scalacenter#497 from olafurpg/disable-signatures (13 hours ago)
|10| 7930947 -- DisableSyntax add regex (13 hours ago)
|11| 5dbdd6b -- IntervalSet test for empty and add toString (13 hours ago)
|12| b022fbd -- DisableSyntax don't repeat DisableSyntax.keyword in message (13 hours ago)
|13| a992b02 -- Assert instead of scalafix:ok (13 hours ago)
|14| 7896ccd -- Refactor Disable to use views. (13 hours ago)
|15| 58acdbe -- Fix scalacenter#493, handle synthetics and symbol signatures in Disable. (13 hours ago)
|16| b48d7f0 -- Merge pull request scalacenter#490 from olafurpg/unmanagedSources (13 hours ago)
|17| e9b2b0a -- s/canFormat/canFix/ (13 hours ago)
|18| 26be6fa -- Use unmanagedSources instead of unmanagedSourceDirectories. (13 hours ago)
|19| 4d46001 -- Merge pull request scalacenter#488 from olafurpg/master (13 hours ago)
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants