-
Notifications
You must be signed in to change notification settings - Fork 148
Auto-detect elm version per file #653
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
base: main
Are you sure you want to change the base?
Conversation
b54dae9 to
324b2a2
Compare
|
To avoid unexpected behavior changes:
|
src/ElmFormat.hs
Outdated
| return $ Right $ concat $ files | ||
|
|
||
|
|
||
| type ElmFile |
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 you prefer a newtype instead? Or no type at all and just using the tuple everywhere?
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.
Yeah, I think either newtype or data with named record fields is preferable.
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.
Ok, I used a data with named record fields.
src/ElmFormat/Filesystem.hs
Outdated
| hasFilename name path = | ||
| name == FilePath.takeFileName path | ||
|
|
||
| addElmVersion :: FileStore f => FilePath -> Free f (FilePath, ElmVersion) |
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.
You might prefer other names for this function and ElmVersion.fromFile, I'm open to suggestions if you don't like them.
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.
Yeah, the name seems good. The one thing I wonder about is how it might work out to have findAllElmFiles return [ElmFile] instead of [FilePath] and having to use addElmVersion later.
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.
Also having findAllElmFiles return [ElmFile] would allow a future optimization of detecting the version at the top and know it already as you recurse down into directories instead of later having to recurse up from each file to find its version.
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 added the optimization in the last version.
|
The following instructions from
Maybe you have a suggestion or this can be done later when preparing the release? |
324b2a2 to
15f3458
Compare
I guess if we're not doing that now (because we want |
src/ElmFormat.hs
Outdated
| do | ||
| let autoYes = Flags._yes config | ||
| resolvedInputFiles <- Execute.run (Execute.forHuman autoYes) $ resolveFiles (Flags._input config) | ||
| detectedElmVersion <- Execute.run (Execute.forHuman autoYes) $ ElmVersion.fromFile "." |
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 this IO action fail? If so, we should maybe postpone running this until we know that detectedElmVersion will be used.
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 still needed in last version, but things have changed a lot, so let's discuss it again based on the new version.
src/ElmFormat.hs
Outdated
| doIt :: (InputConsole f, OutputConsole f, InfoFormatter f, FileStore f, FileWriter f) => ElmVersion -> Flags.Config -> WhatToDo -> Free f Bool | ||
| doIt elmVersion config whatToDo = | ||
| let | ||
| getVersion fileDetectedElmVersion = |
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 feels like it's starting to get messy to me. I'm thinking that we should probably try removing the elmVersion param to doIt and pushing it into each of the WhatToDo constructors that need it, and then we should also resolve the actual versions for each file (this getVersion logic) before calling doIt. What do you think?
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.
Ok, done.
Things could be even simpler if we get rid of global auto-detection and just use 0.19.
To be discussed, see my other comment about that.
src/ElmVersion.hs
Outdated
| _ -> Elm_0_18 | ||
|
|
||
| _ | path == dir -> | ||
| return Elm_0_19 |
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 think this branch means that if there is not elm-package.json, and we've recursed out as far as we can, then give Elm_0_19. Is that right? I think it should probably return Nothing in this case and let the caller decide what to default to rather than hardcoding the default in this function -- is there anything I'm missing about this?
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.
Oh, was this done to only have to check one file per level as you move outward?
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.
Fixed in last version.
src/ElmVersion.hs
Outdated
| do | ||
| let dir = takeDirectory (path) | ||
| elmPackageJson <- stat (path </> "elm-package.json") | ||
| case elmPackageJson of |
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.
Since we are now recursing upward (and it looks like we normalize the path, so we'll recurse up all the way to the root of the filesystem?) so that means we might go up to a folder that contains an elm.json or elm-package.json that is owned by a different user and maybe not readable by the current user. Will stat still succeed in that case? Are there any possible permissions that might cause an error? (Maybe we reach a folder that we can't list? though I'm not sure that would be possible given we are in a folder that's inside it.) If there are possible IO errors from the stat due to files that would be outside of the current user's control, I think we ought to make sure we ignore them rather than crashing.
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.
Ok. This first naive implementation has at least the merits of raising a few potential design issues, so let's slow down and go back to agreeing on what we want, as this does not seem absolutely clear from #561 comments only.
First, there does not seem to be an universal agreement of what normalize means for paths, but System.FilePath.normalise used in the PR does not make a path an absolute one. This can be noticed by the fact that it does not use some IO (Sytem.Path.toAbsoluteFilePath does). System.FilePath.takeDirectory does not make the path absolute either.
and it looks like we normalize the path, so we'll recurse up all the way to the root of the filesystem
We don't make the path an absolute path (the only use of normalise is in TestWorld.hs and this does not make a path absolute as explained previously), so we won't recurse all the way except if the path is already an absolute path.
takeDirectory is only a string manipulation function, it has not real knowledge of the filesystem.
Consequently, for example:
./src/file.elmwill stop at./tmp/file/elmwill stop at/../test.elmwill actually test..then., because maybe surprisinglytakeDirectory ".." == "."(andtakeDirectory "../.." == "..") 🤔
Therefore, if elm-format is run from a directory below the elm.json or elm-package.json file, the version may not be correctly detected.
For example if in a 0.18 package, elm-format . is run from the src sub-directory (instead of the package root directory with the elm-package.json inside), this will return the default value 0.19.
After your review, it seems to me that preferably, the design should be instead:
- For each file argument:
- make it absolute (which is actually also an IO operation that recursively walks upward up to
/) - search for
elm-package.jsonorelm.jsonupward up to/, trying to detect an upward version - then if it is a directory, continue recursively downward to search
elmfiles, detectingelm-package.jsonandelm.jsonon the way, and therefore downward versions (in an optimized way), once per directory.
- make it absolute (which is actually also an IO operation that recursively walks upward up to
From a performance point of view, this would optimize the findAllElmFiles case (a directory argument), but would not optimize the detection of several files arguments, but I think the main use cases are a single file argument or a directory, so this could be acceptable?
A significant downside of this approach is that this would require to really simulate directories in TestWorld.hs, which seems quite daunting and error prone (also what is the directory above . in TestWorld?). An alternative would be to use an existing implementation like fakefs, which seems quite limited also, or to hack a very small working use case for what already exists and do real tests instead for more complicated cases. None of them seems completely compelling but a compromise would have to be found.
Current implementation is simple in TestWorld because we don't transform to absolute paths and only use string operations on paths, which does not require to add filesystem operations in FileStore.hs (only normalise to get rid of ./ prefixes and change / to \ on windows), however test cases should not dictate the design. Maybe I am just lazy 😅.
@avh4, what do you think?
| |> TestWorld.uploadFile "0.19/src/test.elm" "module Main exposing (f)\n\n\nf =\n '\\u{2000}'\n" | ||
| |> TestWorld.uploadFile "0.19/elm.json" "{\"elm-version\": \"0.19.0 <= v < 0.20.0\"}" | ||
| |> run "elm-format" ["0.18/src/test.elm", "0.19/src/test.elm", "--validate"] | ||
| |> expectExit 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.
👍
|
Looks like a great start, thanks! |
|
Ah, well my comments were based on the assumption that There are three main uses of elm-format: use from editors and editor plugins, use from the command line, and use from CI. For editors, is it common for editors to be running elm-format from a working directory that's inside of the folder containing the elm.json? I think probably not, though maybe I'm wrong about that. For command line use, I can imagine someone might |
15f3458 to
3b2edf7
Compare
|
I made a new version that assumes that running elm-format from command line in a sub-directory of the Elm project should work. Consequently, elm version detection is done first upward, then downward during files finding. For now I have kept the global current directory Elm version detection to avoid behavior regressions, but I'm really not sure it makes sense anymore. It seems that the only use case is running in an 0.18 project with |
3b2edf7 to
8addbf1
Compare
| do | ||
| let autoYes = Flags._yes config | ||
| resolvedInputFiles <- Execute.run (Execute.forHuman autoYes) $ resolveFiles (Flags._input config) | ||
| currentDirectoryElmVersion <- Execute.run (Execute.forHuman autoYes) $ FS.findElmVersion "." |
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'm not sure this is useful anymore versus just using 0.19 as the default version.
I kept current working directory global version detection to avoid behavior regressions, but I fail to see valid use cases for it. The only one I see is running in a 0.18 project with --stdin and without --elm-version, is it worth it?
| = ElmFile | ||
| { version :: ElmVersion | ||
| , path :: FilePath | ||
| } deriving (Show) |
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.
Just used deriving Show for my own private tests. It can be removed if you want.
| doesFileExist = Dir.doesFileExist | ||
| doesDirectoryExist = Dir.doesDirectoryExist | ||
| doesFileExist path = do | ||
| exists <- try (Dir.doesFileExist path) :: IO (Either IOException Bool) |
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 to avoid exceptions if detecting the version upward fails (because of permissions issues or something).
Maybe you would have preferred using try inside the Free Monad?
If so I would need guidance as I'm not sure how to do this with Free monad.
| renderInfo (FileWouldChange file) = | ||
| putStrLn $ "File would be changed " ++ file | ||
| renderInfo (FileWouldChange file version) = | ||
| putStrLn $ "File would be changed " ++ file ++ " (" ++ show version ++ ")" |
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.
Is it ok to you? It seems useful to add the detected version in output.
| return False | ||
|
|
||
| makeAbsolute path = | ||
| -- wrong but enough for tests |
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.
Is it ok to you? I'm not sure that writing a lot of code for this now is worth it.
f8a2339 to
244172d
Compare
| determineWhatToDoFromConfig config defaultElmVersion resolvedInputFiles = | ||
| do | ||
| source <- determineSource (Flags._stdin config) resolvedInputFiles | ||
| checkUpgradeVersion (Flags._upgrade config) (Flags._elmVersion 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.
In 0.8.2, upgrade can actually use the auto-detected current directory elm version, which seems confusing to me (ie: --upgrade is possible without --elm-version):
MustSpecifyVersionWithUpgrade says:
"I can only upgrade code to specific Elm versions. To make sure I'm doing what you expect, you must also specify --elm-version=" ++ show elmVersion ++ " when you use --upgrade."
This check makes sure that a flag is provided, whatever the result of "current directory elm version" (which might be removed) or "per file version detection" (which is overridden by the flag).
I'm not sure if this is the expected behavior.
244172d to
c09e060
Compare
| (True, Elm_0_18) -> | ||
| Elm_0_18_Upgrade | ||
|
|
||
| (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.
checkUpgradeVersion makes actually impossible to use something else than 0.19.
Still, the whole upgrade version code (check and upgrade) seems a little messy, but I'm not sure of the exact behavior we want, nor how to refactor this to avoid splitting the version check and the version upgrade later.
| return $ Either.fromRight False exists | ||
|
|
||
| listDirectory = Dir.listDirectory | ||
| makeAbsolute = Dir.makeAbsolute |
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 might also fail but I'm not sure what to do or if we want to handle this 🤔
Resolves #561