-
-
Notifications
You must be signed in to change notification settings - Fork 646
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
Add plugin for scalafix #4635
Add plugin for scalafix #4635
Conversation
This will need a few more updates (namely a fix for scalacenter/scalafix#176) before landing, so not adding reviewers yet. |
c942fb8
to
5ec8ed0
Compare
This is now reviewable. Will add reviewers once I have green CI. |
BUILD.tools
Outdated
jar_library( | ||
name = 'scalac-plugin-dep', | ||
jars = [jar(org='org.scalameta', name='scalahost_{}'.format(SCALA_REV), rev='1.8.0')], | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to experiment with pre-release versions of scalameta without forking pants?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Tool definitions can be reconfigured on a repo by repo basis.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After our recent release, this should now be jars = [jar(org='org.scalameta', name='scalac-semanticdb_{}'.format(SCALA_REV), rev='2.0.0')],
.
'src/python/pants/goal:task_registrar', | ||
], | ||
distribution_name='pantsbuild.pants.contrib.scalafix', | ||
description='scalafix support for pants.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@olafurpg What is the appropriate capitalization of "scalafix" here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I use capital S at the start of sentences and lower-case inside sentences, same as we do with scalameta.
args.append('--sourceroot={}'.format(results_dir)) | ||
args.append('--classpath={}'.format(':'.join(classpath))) | ||
if self.get_options().config: | ||
args.append('--config={}'.format(self.get_options().config)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@olafurpg What does take precedence: --config or --sourceroot/--classpath?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
--config does not have --classpath or --sourceroot, see
https://github.com/scalacenter/scalafix/blob/9b1b07021ba48a56906bfb26ec86c7cf209d32c7/scalafix-core/src/main/scala/scalafix/config/ScalafixConfig.scala#L14
and
https://github.com/scalacenter/scalafix/blob/9b1b07021ba48a56906bfb26ec86c7cf209d32c7/scalafix-cli/src/main/scala/scalafix/cli/ScalafixOptions.scala#L32
I admit it's a bit confusing to have different systems for cli options and the scalafix configuration.
def _run(self, target, results_dir, classpath): | ||
# We always operate on copies of the files, so we execute in place. | ||
args = ['--in-place'] | ||
args.append('--sourceroot={}'.format(results_dir)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
--sourceroot should point to the root of the build. Is this what results_dir
stands for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is when we are consuming a database, rather than when we're creating it. We're executing the rewrites on files that have been cloned elsewhere (into a private result_dir
), rather that directly in the repo. The sourceroot
at compile time (database creation time) is relative to the root of the repo, but here it is being consumed on separate copies of the files.
This allows for both ScalaFixCheck and ScalaFixFix to be implemented with the same codepaths.
if self.get_options().rewrites: | ||
args.append('--rewrites={}'.format(self.get_options().rewrites)) | ||
if self.get_options().level == 'debug': | ||
args.append('--verbose') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Scalafix expects (or is supposed to expect) input files to be specified explicitly via the -f
option. Is this option set anywhere in the current implementation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-f or --files is optional, unbound trailing arguments are interpreted as files scalafix foo.scala
if self.get_options().rewrites: | ||
args.append('--rewrites={}'.format(self.get_options().rewrites)) | ||
if self.get_options().level == 'debug': | ||
args.append('--verbose') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of scalafix rewrites need a semantic db to be generated for target files. Is it possible / does it make sense to provide sanity checks for that here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you elaborate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this was somewhat ambiguous. What I meant to ask is whether it'd make sense to provide additional safeguards on the Pants side. Iirc, scalafix HEAD says "something something please recompile" if it doesn't find semantic dbs, but this message isn't going to help a Pants plugin user who tries to run scalafix without having a scalahost plugin in the first place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't actually seen the error, so not sure how good it is right now... but would love for the error from scalafix to be sufficiently helpful that I don't need to check for existence here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some sort of error message saying "make sure you set up the scalameta plugin using the blahblah BUILD.tools target or the blahblah option" would be helpful though. And that can't come from scalafix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
scalafix will still in some cases silently fail on misconfigured semanticdbs, scalacenter/scalafix#264 This is a bug, scalafix should report helpful error messages when things go wrong and report the appropriate exit code to accompany it. For example, scalafix will exit with code 32 when it encounters a "stale semanticdb" (file contents doesn't match semanticdb contents).
|
||
def execute(self): | ||
classpaths = self.context.products.get_data('runtime_classpath') | ||
# NB: This task uses only the literal classpath of each target, so does not need |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's literal classpath?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well... it's a thing that I don't have any other name for, frankly.
"transitive": All transitive dependencies of a compile unit.
"direct"/"strict": Only the directly declared dependencies of a compile unit.
"literal"/???: Only the classfiles for the compile unit, and none of its dependencies.
If you have something else that you call that, happy to rename.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it. I just did a bit of googling and didn't find a definition, so decided to ask you to better understand what's going on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like "uses only the classes directly generated from the sources of the target"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, as far as I can tell (no scalafix expertise here...)
It would be nice to have a README.md explaining how to set this up, including the BUILD.tools and pants.ini changes required.
contrib_plugin( | ||
name='plugin', | ||
dependencies=[ | ||
'contrib/node/src/python/pants/contrib/node:plugin', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Derp, thanks.
|
||
def execute(self): | ||
classpaths = self.context.products.get_data('runtime_classpath') | ||
# NB: This task uses only the literal classpath of each target, so does not need |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like "uses only the classes directly generated from the sources of the target"?
if self.get_options().rewrites: | ||
args.append('--rewrites={}'.format(self.get_options().rewrites)) | ||
if self.get_options().level == 'debug': | ||
args.append('--verbose') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some sort of error message saying "make sure you set up the scalameta plugin using the blahblah BUILD.tools target or the blahblah option" would be helpful though. And that can't come from scalafix.
Hah. It looks like (some) of the CI failures are due to duels happening between scalafix and scalafmt, where scalafmt re-writes some files, and then scalafix rewrites them again from a cached copy which reverts the changes. Thinking about how best to address that (hopefully not by disabling caching for the |
@stuhood I've considered consolidating the two into a single cli tool. It should be possible to model scalafmt as a syntactic scalafix rewrite and add support to run scalafmt at the end. Do you think something like that might be useful? |
Heads up @stuhood, I've been polishing the cli lately to make it (hopefully) more intuitive. The main changes relate to how scalafix finds files. In a nutshell, scalafix will no longer infer the .scala files from the --classpath. Instead, you tell scalafix which files to fix and provide a --classpath to tell scalafix where to look for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to ship a test with this but I am okay with it as-is. Excited to try this out.
I'm hoping to release scalafix v0.5 around next week Tuesday. The v0.5 milestone has a few simple remaining issues https://github.com/scalacenter/scalafix/milestone/3 I have still yet to document the main changes, but they mostly are aimed to increase the quality-of-life for the cli so it might be worth the wait for this PR |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
scalafix 0.5.0 is just around the corner, I'm in the process of writing the changelog. Please don't hesitate to ask questions if anything is unclear. Most people in the OSS community use sbt-scalafix so there may be missing details about direct cli usage in the docs.
I can say upfront however that the cli still needs more work on error reporting, esp. related to semanticdb loading.
if self.get_options().rewrites: | ||
args.append('--rewrites={}'.format(self.get_options().rewrites)) | ||
if self.get_options().level == 'debug': | ||
args.append('--verbose') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
scalafix will still in some cases silently fail on misconfigured semanticdbs, scalacenter/scalafix#264 This is a bug, scalafix should report helpful error messages when things go wrong and report the appropriate exit code to accompany it. For example, scalafix will exit with code 32 when it encounters a "stale semanticdb" (file contents doesn't match semanticdb contents).
We learned a bit more from b6cf3e5 about how formatters should work, so I've started down the path of implementing a shared base class for scalafmt and scalafix: now added here. The The final interesting bit that I want to include in this change is that scalafix needs a classpath for some cases: it's become obvious internally that we cannot "always" require a classpath for Unfortunately, I'm out until September 10th, so there won't be any movement from me until at least then. |
Scalafix 0.5.0-RC3 is out http://www.scala-lang.org/blog/2017/09/11/scalafix-v0.5.html, changelog https://github.com/scalacenter/scalafix/releases/tag/v0.5.0-RC3 I think the api (both library and cli flags) will remain stable for a while now, at least I don't see any breaking changes coming up anytime soon. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for your work! I've made some change requests based on our recent Scalameta release.
BUILD.tools
Outdated
jar_library( | ||
name = 'scalac-plugin-dep', | ||
jars = [jar(org='org.scalameta', name='scalahost_{}'.format(SCALA_REV), rev='1.8.0')], | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After our recent release, this should now be jars = [jar(org='org.scalameta', name='scalac-semanticdb_{}'.format(SCALA_REV), rev='2.0.0')],
.
contrib/scalafix/README.md
Outdated
Unlike `scalafmt` (which is included in the default distribution of pants), `scalafix` requires a | ||
compiler plugin, and is thus distributed as a contrib module with extra setup steps. | ||
|
||
The [scalameta](http://scalameta.org/) compiler plugin extracts semantic information at compile |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The [semanticdb](http://scalameta.org/) compiler plugin...
contrib/scalafix/README.md
Outdated
name = 'scalac-plugin-dep', | ||
jars = [jar(org='org.scalameta', name='scalahost_{}'.format(SCALA_REV), rev='1.8.0')], | ||
) | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the suggested change to scalac-plugin-dep above.
contrib/scalafix/README.md
Outdated
] | ||
|
||
scalac_plugins: [ | ||
'scalahost', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'semanticdb-scalac',
contrib/scalafix/README.md
Outdated
|
||
## Usage | ||
|
||
Tasks for both the `fmt` and `lint` goals are provided, each with the same set of options. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about calling these goals fix
and check
in accordance with how these actions are called in Scalafix? https://github.com/scalacenter/scalafix/blob/master/scalafix-core/shared/src/main/scala/scalafix/rule/Rule.scala.
def register_options(cls, register): | ||
super(ScalaFix, cls).register_options(register) | ||
register('--skip', type=bool, fingerprint=True, | ||
help='Skip running scalafix.') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this option?
classpaths = self.context.products.get_data('runtime_classpath') | ||
# NB: While this task uses only the classes directly generated from the sources of each | ||
# target, the content of the generated semantic DBs will be affected by changes to | ||
# dependencies. Thus, we use invalidate_dependents=True. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very good point!
invalidate_dependents=True) as invalidation_check: | ||
for vt in invalidation_check.all_vts: | ||
if not vt.valid: | ||
self.context.log.debug('Fixing {}...'.format(vt.target.address.spec)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May I suggest changing the verb here depending on the goal that is being executed?
pants.ini
Outdated
] | ||
no_warning_args: [ | ||
'-S-nowarn', | ||
] | ||
|
||
scalac_plugins: [ | ||
'scalahost', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'semanticdb-scalac',
JarDependency(org='com.geirsson', | ||
name='scalafmt-cli_2.11', | ||
rev='1.0.0-RC4') | ||
]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like an outdated version.
shutil.copy(src, dst) | ||
|
||
# Execute. | ||
result = self.runjava(classpath=self.tool_classpath('scalafix'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Am I right in thinking that scalafix
is supposed to be bundled with Pants?
Alright, I've made a bunch of changes:
I'm going to apply @xeno-by's feedback with regard to versioning. I also think that I can remove the contrib module and move this into core by changing the default of the |
…actual resolution from an ivy-style repository.
…ests when scalafix is skipped.
…ermetic tests to pass.
65fb5f4
to
faa5ae4
Compare
…ight still be exploding on a bad match...?
5a0ed74
to
8a92954
Compare
Test flakiness. Merging. |
Problem
scalafix supports a bunch of type-aware rewrites of scala code using scalameta, but pants does not currently support it.
Solution
Add support for
scalafix
in core, with support for semantic rewrites disabled by default. To enable semantic rewrites, it requires an scala compiler plugin, and so enabling it requires extra setup documented here in theREADME.md
for scala.Result
Running something like:
would strip unused imports and results in diffs like: