-
Notifications
You must be signed in to change notification settings - Fork 148
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
Auto-detect elm version per file #653
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
|
||
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
@@ -63,3 +64,7 @@ findAllElmFiles inputFile = | |||
hasFilename :: String -> FilePath -> Bool | |||
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
@@ -223,6 +229,7 @@ main'' elmFormatVersion_ experimental_ args = | |||
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.elm
will stop at.
/tmp/file/elm
will stop at/
../test.elm
will 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.json
orelm.json
upward up to/
, trying to detect an upward version - then if it is a directory, continue recursively downward to search
elm
files, detectingelm-package.json
andelm.json
on 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
@@ -222,9 +228,11 @@ main'' elmFormatVersion_ experimental_ args = | |||
Just config -> | |||
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.
|
||
doesDirectoryExist _path = | ||
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
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.
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