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

Introduce wrap-ignore-errors #317

Merged
merged 3 commits into from Jul 8, 2021
Merged

Introduce wrap-ignore-errors #317

merged 3 commits into from Jul 8, 2021

Conversation

vemv
Copy link
Member

@vemv vemv commented Jul 3, 2021

Fixes #291

Interestingly, merely by adding intentionally-invalid .clj files to test-resources, all sorts of tests/functionality in the codebase started to break. This surfaced that wrap-ignore-errors is pretty necessary, as otherwise it doesn't seem ideal that the occasional odd file in the source path will render refactor-nrepl useless.

This is particularly true for end users that may have not explicitly set the paths to be analyzed (i.e. they may trying to analyze a whole project, not just a subset like src/test).

The README already advises to set :ignore-errors, so now with this improvement that's more true than ever.

@expez
Copy link
Member

expez commented Jul 3, 2021

I'm strongly opposed to footguns like this (and all our other ignore-errors functionality).

When the user turns on M-x yolo-mode we have no way of distinguishing between actual errors and errors that they want us to DWIM and ignore.

If they have one or more errors in their project that prevents actual analysis and then run cljr-rename-symbol we'll do a half-finished job and their only recourse (because we don't have an undo for this operation) is:

  • Git reset --hard HEAD
  • Manually finishing / undoing the job

In a perfect world everyone would be doing red, green, refactor, but that's not how the majority of us works. I for one will very often be working on X and then go off on a tangent to refactor something. If my work on X was incomplete to the point where clj-refactor can't reliable execute my requested refactoring I NEVER want it to just YOLO on. That will turn a 5 second tangent into a huge detour that will drop me out of my state of flow completely.

As far as #291 goes I believe a much better solution would be to let this op too take into account the ignore-paths config setting that let's the user tell us about e.g. their template files.

edit: man, this brings back memories. Benedek and I went so many rounds on ignore-errors back in the day. Happy to have a new audience for my rants 🤣

@bbatsov
Copy link
Member

bbatsov commented Jul 4, 2021 via email

@vemv
Copy link
Member Author

vemv commented Jul 4, 2021

Why don’t you use with-safe-transport instead?

Thanks for bringing it up. I don't think it tackles exactly the same problem: with-safe-transport informs of exceptions, while this PR allows functionality to work.

...I'm aware that an end user, after being informed by with-safe-transport of a stacktrace, could fix the corresponding root cause, retry the op and get it to work.

But I think that for a very significant array of use cases, things should work OOTB. I'll write up a refined proposal following @expez' remarks which are certainly useful!

@bbatsov
Copy link
Member

bbatsov commented Jul 4, 2021 via email

@expez
Copy link
Member

expez commented Jul 4, 2021

In the context of cljr-slash in particular I believe the default failure mode should be to fail silently, and just gracefully degrade to simply inserting the / character. However, like @vemv discovered it would also be super useful to be able to toggle explicit errors on (which you can't right now) if you notice the op stops working.

My reasoning behind suppressing errors is that we're hijacking the / button and if we raise an error every time that fails and the user can't, won't or doesn't have time to fix the root cause, it's going to be super annoying real fast.

@vemv
Copy link
Member Author

vemv commented Jul 5, 2021

Firstly, it's worth emphasizing the focus of this PR: it ignores errors while building a corpus (namely: reading/requiring files, and sometimes building an AST out of them). It does not propose wrapping the operations themselves (e.g. rename-file-or-dir) in a try/catch.

Building a corpus is somewhat prone to errors whenever the whole project (i.e. not just :source-paths/:test-paths) is being analysed. Which can quite plausibly happen in face of the following considerations:

  • users may not be even aware of what refactor-nrepl is, especially newcomers or people with different priorities at work
    • i.e. it's generally a part of CIDER, so why would a user bother configuring something that is abstracted away by CIDER?
    • a good default for this kind of user is working OOTB.
  • ignore-paths is a blacklist, not a whitelist
    • blacklists are always subject to become outdated by new dirs in a given project
    • also, importantly, users might work with dozens of projects
      • can we realistically expect that ignore-paths is accurately configured for each of them?
    • refactor-nrepl should ideally work OOTB for arbitrary projects, e.g. a random git clone for something I'm checking out transiently
  • Not everyone sets the refresh-dirs via tools.namespace
    • This would simplify some things (since now r-n has some degree of t.n integration)
    • but people may not use tools.namespace or leave it with refresh-dirs unconfigured

So, long story short, it's more than plausible that refactor-nrepl can choke on irrelevant files. I don't think that not working (sometimes opaquely) is a good behavior.

Even if refactor-nrepl did accurately inform of the problematic file, asking people to configure their project feels pedantic and may not succeed. One can argue, people want things to 'just work', not a tool give to them more problems :) especially for the mentioned use cases: newcomers, people who work with a lot of projects, or people who prioritize other things over spending time with fine-grained tooling settings.

Anyway, I don't think that the segment of the user base that actually configures things carefully should pay any price for this leniency. So based on @expez input I thought of...

strict-mode

strict-mode would simply a boolean option. Its value would translate directly to the wrap-ignore-errors boolean parameter.

The interesting part being, strict-mode would be set dynamically following the following rules/heuristics:

  • if strict-mode is explicitly set, honor that; else:
  • decide its value depending on whether ignore-paths has elements, refresh-dirs is set, etc.
    • i.e. any sign that the user knows what he's doing would be taken into account, else we default to leniency.

WDYT?

Between the limited scope of the changes (corpus building) and optionality/smartness of a strict-mode option, this could be a net win in terms of user experience.

@vemv
Copy link
Member Author

vemv commented Jul 5, 2021

As one last note, sometimes ASTs can't be built purely becase tools.analyzer errors (bugs). e.g. core.async stuff.

But one can make a sensible (although not risk-free) bet: the code that made t.ana choke was irrelevant for the op in question (e.g. it wouldn't be renamed at all by rename-file-or-dir).

Right now, rename-file-or-dir would fail 100% of the time in face of a problematic file. With the proposed changes, it would fail < 100% of the time.

The result might be slightly invalid, but the user could either fix that manually or discard the results altogether. I don't think that's too much of an ask: no IDE's "rename" is perfect so a seasoned user will have some cautions around automated refactoring (e.g. git-commit prior work, run tests before committing, etc)

@expez
Copy link
Member

expez commented Jul 5, 2021

This is probably the one hill I'd be willing to die on when it comes to this project. The setting that allows refactor-nrepl to perform erroneous / incomplete refactorings across your project has to be opt-in.

why would a user bother configuring something that is abstracted away by CIDER?

FWIW, we're not bundled along with CIDER. @benedekfazekas did an amazing job of porting the refactorings that don't require the middleware (e.g. introduce-let and expand-let) to clojure-mode, but the rest has to be explicitly installed.

users may not be even aware of what refactor-nrepl is, especially newcomers or people with different priorities at work

I guess this is the gist of my disagreement about strict-mode and ignore-errors. The target audience of refactor-nrepl is the same as Clojure itself: experts willing to learn the best tool for the job. Making tools for beginners and the disinterested is not where it's at. Instead we want to target the users that are willing to read a short README, learn some new keybindings and maybe even set a variable or two. These are the people that are into Emacs and Clojure and that seek this project out.

can we realistically expect that ignore-paths is accurately configured for each of them?

Many Clojure projects will have at least one Emacs enthusiast and they will add a .dir-locals.el which configures Emacs.

Right now, rename-file-or-dir would fail 100% of the time in face of a problematic file. With the proposed changes, it would fail < 100% of the time.

This line of reasoning is great, because I think we have a lot of ways to sidestep reliability problems related to tools.analyzer if we go on an op-by-op basis. E.g. as you mention rename-file-or-dir it occurs to me that we could probably rely on the same logic that's used by clean-ns to find used symbols instead of tools.analyzer.

The result might be slightly invalid, but the user could either fix that manually or discard the results altogether. I don't think that's too much of an ask: no IDE's "rename" is perfect so a seasoned user will have some cautions around automated refactoring (e.g. git-commit prior work, run tests before committing, etc)

In a previous life I wrote enterprise Java code and I can't recall experiencing Intellij making mistakes on a regular basis. If I ever saw that I would upgrade to the latest version and/or file a bug report. Teaching people to alter their workflow to accommodate the tool is IMO undesirable to begin with but also about as hard as teaching them to configure their tool correctly (which they only have to do once).

@expez
Copy link
Member

expez commented Jul 5, 2021

Firstly, it's worth emphasizing the focus of this PR: it ignores errors while building a corpus (namely: reading/requiring files, and sometimes building an AST out of them). It does not propose wrapping the operations themselves (e.g. rename-file-or-dir) in a try/catch.

Yeah, I don't have any objections to this PR. We already have ignore-errors and this looks like an excellent extension of that functionality. I'll give it more thorough read-through and approve (probably tomorrow morning).

I realize I sent us off on a bit of tangent, and I apologize for that, but this line:

The README already advises to set :ignore-errors, so now with this improvement that's more true than ever.

really triggered me 🤣

Copy link
Member

@expez expez left a comment

Choose a reason for hiding this comment

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

Great work on this! 👍

@@ -36,13 +37,16 @@
;; corner case - use the mranderson-ized refresh-dirs (for supporting this project's test suite):
refresh-dirs))

(def default-predicate (every-pred core/source-file?
Copy link
Member

Choose a reason for hiding this comment

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

default-file-predicate or maybe even something like filter-file-predicate? The latter gives some indication that we're using this predicate to filter the files to be processed.

(f x)
(catch Exception e
(maybe-log-exception e)
;; return false, because `wrap-ignore-errors` is oriented for predicate usage:
Copy link
Member

Choose a reason for hiding this comment

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

the trailing : in this comment makes it seem like something is missing

Copy link
Member Author

@vemv vemv Jul 6, 2021

Choose a reason for hiding this comment

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

the trailing : in this comment makes it seem like something is missing

I add the colon to make it clear that the comment refers to the next LOC. Else there can be an ambiguity (does it refer to the previous one, to the defn in general, etc?)

I guess it failed at that 😇

aliases-by-frequencies)})
([]
(namespace-aliases false))

Copy link
Member

Choose a reason for hiding this comment

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

Extra line here

Copy link
Member Author

Choose a reason for hiding this comment

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

Will rm it

(fwiw it was there on purpose... I don't like "shim" arities to clutter the "impl" arity, so that I can read the impl better)

(defn project-files-in-topo-order
([]
(project-files-in-topo-order false))

Copy link
Member

Choose a reason for hiding this comment

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

Extra blank line here


([old-path new-path]
(rename-file-or-dir old-path new-path false))

Copy link
Member

Choose a reason for hiding this comment

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

Extra line here

sym-by-file&fullname)})
([]
(referred-syms-by-file&fullname false))

Copy link
Member

Choose a reason for hiding this comment

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

Extra line here

CHANGELOG.md Outdated
@@ -2,6 +2,18 @@

## Unreleased

#### Changes
* The `:ignore-errors` option will be honored in more places, making refactor-nrepl more robust in face of files not particularly meant to be part of the AST corpus.
Copy link
Member

Choose a reason for hiding this comment

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

I'd also document this more prominently somewhere, as there are a couple of other editors using refactor-nrepl.

CHANGELOG.md Outdated
#### Changes
* The `:ignore-errors` option will be honored in more places, making refactor-nrepl more robust in face of files not particularly meant to be part of the AST corpus.
* Examples: WIP files, Moustache template files, scripts.
* Closes https://github.com/clojure-emacs/refactor-nrepl/issues/291
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 be a prefix of the parent CL entry.

CHANGELOG.md Outdated
* Upgrade Orchard
* Worth emphasizing: now the following options are available https://github.com/clojure-emacs/orchard/tree/v0.7.0#configuration-options
* They can make the refactor-nrepl experience simpler / more robust.
* Reliabilty improvement: try using `require` prior to `find-ns`
Copy link
Member

Choose a reason for hiding this comment

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

There's a typo in reliability

@@ -0,0 +1,29 @@
(ns refactor-nrepl.unreadable-files
Copy link
Member

Choose a reason for hiding this comment

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

You can also add some ns comment about the purpose of the file.

@@ -72,3 +72,14 @@
(defn maybe-log-exception [^Throwable e]
(when (System/getProperty "refactor-nrepl.internal.log-exceptions")
(.printStackTrace e)))

(defn wrap-ignore-errors [f ignore-errors?]
Copy link
Member

Choose a reason for hiding this comment

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

A docstring is in order for everything part of the public API.

@bbatsov
Copy link
Member

bbatsov commented Jul 6, 2021

I've got one concern with the wrap-ignore-errors name - we typically use wrap-something for the names of middleware. Perhaps just ignore-errors or with-suppressed/ignored-errors.

@vemv
Copy link
Member Author

vemv commented Jul 6, 2021

FWIW, we're not bundled along with CIDER. [...] has to be explicitly installed.

For completeness, I was referring to this passage of the readme: If you're using CIDER and clj-refactor you don't have to do anything except call cider-jack-in. The dependencies are injected automagically.

The target audience of refactor-nrepl is the same as Clojure itself: experts willing to learn the best tool for the job. Making tools for beginners and the disinterested is not where it's at.

I appreciate this thinking and most definitely wouldn't want to bend the project to accomodate "everyone".

But perhaps by appealing only to an "elite" persona we'd unnecessarily lose many people's trust (by giving a perceived-buggy experience by default, as argued before).

It's not the same to target 40% of devs than 5%. 5% is too little an can end up being counterproductive.

Anyway thanks for the willingness 🤝

Copy link
Member Author

@vemv vemv left a comment

Choose a reason for hiding this comment

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

@expez , would you want something like "strict-mode" added to this PR?

Personally I'd actually like it since I wouldn't want gratuitous swallowing either

aliases-by-frequencies)})
([]
(namespace-aliases false))

Copy link
Member Author

Choose a reason for hiding this comment

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

Will rm it

(fwiw it was there on purpose... I don't like "shim" arities to clutter the "impl" arity, so that I can read the impl better)

(f x)
(catch Exception e
(maybe-log-exception e)
;; return false, because `wrap-ignore-errors` is oriented for predicate usage:
Copy link
Member Author

@vemv vemv Jul 6, 2021

Choose a reason for hiding this comment

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

the trailing : in this comment makes it seem like something is missing

I add the colon to make it clear that the comment refers to the next LOC. Else there can be an ambiguity (does it refer to the previous one, to the defn in general, etc?)

I guess it failed at that 😇

@expez
Copy link
Member

expez commented Jul 7, 2021

@expez , would you want something like "strict-mode" added to this PR?

Not really, this PR already contains quite a bit of code and tons of discussion.

I also don't like the idea of strict-mode in general but that discussion is perhaps better moved to a separate issue at this point, if you want to pursue that further.

This probably goes without saying but strict-mode can obviously happen even if I personally don't think it's worth doing. If you're going to be taking on a more active role in the project going forward, @vemv, then it's only fair that your opinion carries more weight. I'd much rather see this project get actively maintained again, and potentially grow some features I disagree with, than seeing it fall into disrepair.

@vemv
Copy link
Member Author

vemv commented Jul 7, 2021

I also don't like the idea of strict-mode in general but that discussion is perhaps better moved to a separate issue at this point, if you want to pursue that further.

Yeah always sounds good to seek agreement in advance 👍

This probably goes without saying but strict-mode can obviously happen even if I personally don't think it's worth doing. If you're going to be taking on a more active role in the project going forward, @vemv, then it's only fair that your opinion carries more weight. I'd much rather see this project get actively maintained again, and potentially grow some features I disagree with, than seeing it fall into disrepair.

Ok cheers 🍻 likewise I'd much care about the other maintainer being happy with our creation :)

Copy link
Member Author

@vemv vemv left a comment

Choose a reason for hiding this comment

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

PR ready again, will squash the new commit as the last step

(defn- rename-file-or-dir-reply [{:keys [transport old-path new-path] :as msg}]
(reply transport msg :touched (@rename-file-or-dir old-path new-path)
(defn- rename-file-or-dir-reply [{:keys [transport old-path new-path ignore-errors] :as msg}]
(reply transport msg :touched (@rename-file-or-dir old-path new-path (= ignore-errors "true"))
Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure if I should compare against true or "true" here?

...Checking out the codebase, I see a handful of "true" handlings. But there's also a boolean coercion in config.clj which makes me hesitate

Copy link
Member

@expez expez Jul 7, 2021

Choose a reason for hiding this comment

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

This is correct, the stuff that's in the msg hasn't yet been coerced to boolean.

I suppose we could've done a better job of consistently doing coercions in this ns (like you're doing now) instead of passing the msg on as is.

@bbatsov
Copy link
Member

bbatsov commented Jul 8, 2021

I've got one concern with the wrap-ignore-errors name - we typically use wrap-something for the names of middleware. Perhaps just ignore-errors or with-suppressed/ignored-errors.

@vemv Did you see this remark?

@vemv
Copy link
Member Author

vemv commented Jul 8, 2021

Did you see this remark?

Addessed now. Had left it for later as it escaped my eye at first.


Thanks for the feedback both, hope this is, as foreseen, a net gain. Anyway I'll create an issue trying to refine the "strict-mode" idea (might have to be more fine-grained)

@vemv vemv merged commit b78e354 into master Jul 8, 2021
@vemv vemv deleted the vemv--291 branch July 8, 2021 15:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

namespace-aliases could guard against unreadable files
3 participants