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

Update: allow autofixing when using processors (fixes #7510) #9090

Merged
merged 8 commits into from Sep 15, 2017

Conversation

not-an-aardvark
Copy link
Member

@not-an-aardvark not-an-aardvark commented Aug 8, 2017

What is the purpose of this pull request? (put an "X" next to item)

[x] Add something to the core

What changes did you make? (Give an overview)

This implements the proposal from #7510 (comment) to allow autofixes to be applied when using processors.

The processor API basically remains the same. The only changes are:

  • Processors can now export a supportsAutofix: true property, which opts a processor into autofixing.
  • If a processor supports autofixing, the postprocess method is expected to also transform autofix ranges as necessary when it transforms the locations of reported problems. Afterwards, the transformed fixes are applied to the original, unprocessed text.

Multipass autofixing works by calling preprocess and postprocess for each pass.

(One alternative approach would be to run multipass autofixing on the processed text, and only return the fixed text to the postprocessor afterwards. As discussed in #7510, though, that would make it difficult for postprocessors because they would need to figure out how to split the fixed text into code blocks again, which would require some sort of imprecise diffing against the original text.)

Summary of changes:

  • linter.verify and linter.verifyAndFixnow accept optional preprocess and postprocess arguments for splitting up the text into code blocks and combining the messages from each code block. By default, the text is "preprocessed" into a single code block containing the code, and the problems are "postprocessed" by flattening the array. (This is the same as not applying a processor at all.)
  • Applying processors is now is the responsibility of Linter rather than CLIEngine. (However, determining which processors need to be applied for a given file is still the responsibility of CLIEngine.)

TODO:

  • Add more tests
  • Add documentation
  • Get feedback on this API from people who have implemented processors (cc @BenoitZugmeyer, @btmills, others?)
  • Approve this API in a TSC meeting

Is there anything you'd like reviewers to focus on?

  • Does the API seem reasonable?
  • Does the API provide enough information to processors to allow them map autofix ranges correctly?
  • Is this a breaking change? (It seems like if a processor currently doesn't apply a transformation to fix ranges, then the original fix ranges will get used, which might be wildly incorrect and result in broken code.) One way to avoid this could be to have processors opt-in to allowing autofixes.
  • Does it make sense to allow postprocess without preprocess? The default postprocess (flattening the array of problems) will almost always do the wrong thing if multiple code blocks are being linted, since some of the locations will probably be invalid. It might be better to just throw a validation error.
  • This effectively allows plugins to remove or add any message to the user's reported problems list, with no opt-in. Are we sure we're okay with that? (Actually, that was already possible with processors before this change, but processors haven't gotten a lot of use because they prevent autofixing. Now that they don't have downsides, I can imagine that plugins could use them just for the purpose of modifying the problems list.)

@not-an-aardvark not-an-aardvark added core Relates to ESLint's core APIs and features do not merge This pull request should not be merged yet enhancement This change enhances an existing feature of ESLint evaluating The team will evaluate this issue to decide whether it meets the criteria for inclusion labels Aug 8, 2017
@mention-bot
Copy link

@not-an-aardvark, thanks for your PR! By analyzing the history of the files in this pull request, we identified @gyandeeps, @mysticatea and @nzakas to be potential reviewers.

@eslintbot
Copy link

LGTM

1 similar comment
@eslintbot
Copy link

LGTM

@BenoitZugmeyer
Copy link
Contributor

That's great! I didn't look at the implementation in details yet, but I'll try to adapt my plugin to work with this branch, to give you my feedbacks if needed.

@eslintbot
Copy link

LGTM

@not-an-aardvark not-an-aardvark removed the do not merge This pull request should not be merged yet label Aug 20, 2017
@not-an-aardvark not-an-aardvark changed the title [WIP] Update: allow autofixing when using processors (fixes #7510) Update: allow autofixing when using processors (fixes #7510) Aug 20, 2017
@not-an-aardvark
Copy link
Member Author

Thoughts after writing a simple implementation for this as part of the tests:

  • Does linter.verify actually need processor options, or should the options just be added to linter.verifyAndFix? It seems like the only reason Linter needs processors at all is to apply them between autofix passes, since everything else could be done by the consumer of Linter. On the other hand, it would be nice to keep the API consistent.
  • Does it make sense to allow postprocess without preprocess? The default postprocess (flattening the array of problems) will almost always do the wrong thing if multiple code blocks are being linted, since some of the locations will probably be invalid. It might be better to just throw a validation error.
  • Is it worth adding preprocess and postprocess options to RuleTester? It would be possible to simulate those options outside of RuleTester, but it might be nice to have them as a convenience feature.
  • This effectively allows plugins to remove or add any message to the user's reported problems list, with no opt-in. Are we sure we're okay with that? (Actually, that was already possible with processors before this change, but processors haven't gotten a lot of use because they prevent autofixing. Now that they don't have downsides, I can imagine that plugins could use them just for the purpose of modifying the problems list.)

@eslintbot
Copy link

LGTM

@eslintbot
Copy link

LGTM

@eslintbot
Copy link

LGTM

@not-an-aardvark not-an-aardvark added the tsc agenda This issue will be discussed by ESLint's TSC at the next meeting label Aug 31, 2017
@not-an-aardvark
Copy link
Member Author

TSC Summary: This issue proposes allowing autofixing to occur when using processors, by having processors modify autofix locations as necessary as part of the postprocess method.

TSC Question: Should we accept this proposal? If so, should we make this available to processors by default, or add an opt-in for a processor to declare that it supports autofixing?

Copy link
Member

@ilyavolodin ilyavolodin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I'm fine with those changes, however, I don't think they will cover all use-cases. And in some cases, they will make it impossible to restore code back into original format, I think. Like when you had to substitute some variables/names in preprocessor. You might not be able to find them again in postprocessor after fixes.

const fixedResult = linter.verifyAndFix(text, config, {
filename,
allowInlineConfig,
fix: typeof fix !== "undefined" && fix,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would result in performance impact, I would guess. Now we are trying to fix every error, even if we don't need to.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so -- SourceCodeFixer has a fast path to just skip all fixes when fix: false is used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean in the case of processor not supporting autofixing (and not wanting to support it in the future). Autofixing will still happen, but it's results will be ignored by postprocess function.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see. This could be addressed by making autofixing opt-in for processors (#9090 (comment)). Currently, fixes would still get applied by ESLint in this case, but the processor would usually generate broken code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opt-in sounds fine to me.

lib/linter.js Outdated
* Mostly useful for testing purposes.
* @param {boolean} [filenameOrOptions.allowInlineConfig] Allow/disallow inline comments' ability to change config once it is set. Defaults to true if not supplied.
* Useful if you want to validate JS without comments overriding rules.
* @param {function(string): string[]} [filenameOrOptions.preprocess] preprocessor for source text. If provided, this should
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a public API, this would make it a breaking change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate on why it would be a breaking change? It seems like this is just adding additional functionality, not breaking any existing functionality.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry, for some reason I though it's a new argument, not a new property on the object. I'm not really sure how to treat those. On one hand it's a change of a signature of publicly available API, on the other - JS works without it just fine, so it's not a breaking change, I guess.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's any ambiguity here. Code that would previously work when using the API will still work, so it's not a breaking change. (This would also be the case if a new argument was added.)

lib/linter.js Outdated
const preprocess = filenameOrOptions && filenameOrOptions.preprocess || (rawText => [rawText]);
const postprocess = filenameOrOptions && filenameOrOptions.postprocess || lodash.flatten;

return postprocess(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked it a lot more when processors were limited to cli-engine, honestly, but I think that's a personal preference.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I share @ilyavolodin's preference here - organizationally, it feels like Linter shouldn't know about processors. I don't have a specific rule of thumb or design pattern or anything like that to support that feeling, unfortunately - maybe separation of concerns, but one could argue that's too broad.

However, #9090 (comment) clarified for me that postprocess needs to be run in between getting errors in verify and applying fixes. I don't see an easy way around that, so thankfully preprocess and postprocesscan be treated as black boxes from Linter's perspective.

Copy link
Member

@btmills btmills left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is on the right track. Other than making autofixing opt-in, I don't see any hangups. I'll give this another pass once that change is made. Nice work @not-an-aardvark!

@@ -80,7 +80,7 @@ processors: {
The `preprocess` method takes the file contents and filename as arguments, and returns an array of strings to lint. The strings will be linted separately but still be registered to the filename. It's up to the plugin to decide if it needs to return just one part, or multiple pieces. For example in the case of processing `.html` files, you might want to return just one item in the array by combining all scripts, but for `.md` file where each JavaScript block might be independent, you can return multiple items.

The `postprocess` method takes a two-dimensional array of arrays of lint messages and the filename. Each item in the input
array corresponds to the part that was returned from the `preprocess` method. The `postprocess` method must adjust the location of all errors and aggregate them into a single flat array and return it.
array corresponds to the part that was returned from the `preprocess` method. The `postprocess` method must adjust the location of all errors, (including the range in the `fix` property, if present), and aggregate them into a single flat array and return it.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting that the text about adjusting the fix property, inserted after "must adjust the location of all errors" will need to change once processor autofixing is made opt-in - perhaps by moving it into its own sentence.


1. This option throws an error when code is piped to ESLint.
1. This option has no effect on code that uses processors.
This option instructs ESLint to try to fix as many issues as possible. The fixes are made to the actual files themselves and only the remaining unfixed issues are output. Not all problems are fixable using this option, and this option throws an error when code is piped to ESLint.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting that this will also need to change slightly once autofixing in processors becomes opt-in to mention that a processor might not support it.

lib/linter.js Outdated
@@ -1181,6 +1210,11 @@ class Linter {
* @param {boolean} options.allowInlineConfig Flag indicating if inline comments
* should be allowed.
* @param {boolean|Function} options.fix Determines whether fixes should be applied
* @param {Function} options.preprocess preprocessor for source text. If provided, this should
* this should accept a string of source text, and return an array of code blocks to lint.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"this should" is duplicated at the end of the previous line and the beginning of this line.

lib/linter.js Outdated
const preprocess = filenameOrOptions && filenameOrOptions.preprocess || (rawText => [rawText]);
const postprocess = filenameOrOptions && filenameOrOptions.postprocess || lodash.flatten;

return postprocess(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I share @ilyavolodin's preference here - organizationally, it feels like Linter shouldn't know about processors. I don't have a specific rule of thumb or design pattern or anything like that to support that feeling, unfortunately - maybe separation of concerns, but one could argue that's too broad.

However, #9090 (comment) clarified for me that postprocess needs to be run in between getting errors in verify and applying fixes. I don't see an easy way around that, so thankfully preprocess and postprocesscan be treated as black boxes from Linter's perspective.

@not-an-aardvark
Copy link
Member Author

not-an-aardvark commented Sep 4, 2017

Thanks for the reviews!

Bikeshed: It looks like there's consensus that processors should have to opt-in to autofixing. What should the API for that look like? Right now, I'm thinking that processors could have a boolean supportsAutofix: true property in addition to preprocess and postprocess, and CLIEngine would verify that property is true before enabling autofix mode for Linter.

@ilyavolodin
Copy link
Member

A boolean in the exported properties sounds good to me. One other question I have based on your list of things happening in this PR. How does this affect multi-pass autofixing? If we don't modify the code directly, we can't do multi-pass, however, if we do modify the code, postprocessor might not be able to figure out how to change it back to the original format (as I outlined in my comment).

@not-an-aardvark
Copy link
Member Author

not-an-aardvark commented Sep 6, 2017

I described how multi-pass autofixing works in the original PR and this comment. Basically, preprocess and postprocess are run for on each pass, and then ESLint applies the postprocessed fixes to the original code. Is there a particular part of the explanation that you're confused about?

@eslintbot
Copy link

LGTM

@eslintbot
Copy link

LGTM

@ilyavolodin
Copy link
Member

@not-an-aardvark Sorry, I forgot that original comments contained the explanation. That answers my question, and I think it should work just fine.

Copy link
Member

@btmills btmills left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on this, @not-an-aardvark. Makes sense to me 👌

Copy link
Member

@kaicataldo kaicataldo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work @not-an-aardvark!

lib/linter.js Outdated
* Mostly useful for testing purposes.
* @param {boolean} [filenameOrOptions.allowInlineConfig] Allow/disallow inline comments' ability to change config once it is set. Defaults to true if not supplied.
* Useful if you want to validate JS without comments overriding rules.
* @param {function(string): string[]} [filenameOrOptions.preprocess] preprocessor for source text. If provided, this should
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: this should is repeated in this comment

@eslintbot
Copy link

LGTM

@eslintbot
Copy link

LGTM

@not-an-aardvark
Copy link
Member Author

This was accepted in today's TSC meeting.

@not-an-aardvark not-an-aardvark added accepted There is consensus among the team that this change meets the criteria for inclusion and removed evaluating The team will evaluate this issue to decide whether it meets the criteria for inclusion tsc agenda This issue will be discussed by ESLint's TSC at the next meeting labels Sep 14, 2017
@not-an-aardvark not-an-aardvark merged commit 0c720a3 into master Sep 15, 2017
@not-an-aardvark not-an-aardvark deleted the processor-autofixes branch September 15, 2017 01:15
@eslint-deprecated eslint-deprecated bot locked and limited conversation to collaborators Mar 15, 2018
@eslint-deprecated eslint-deprecated bot added the archived due to age This issue has been archived; please open a new issue for any further discussion label Mar 15, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
accepted There is consensus among the team that this change meets the criteria for inclusion archived due to age This issue has been archived; please open a new issue for any further discussion core Relates to ESLint's core APIs and features enhancement This change enhances an existing feature of ESLint
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants