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

Auto-detect elm version per file #653

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

rlefevre
Copy link
Contributor

Resolves #561

@rlefevre rlefevre force-pushed the autodetect-elm-version-per-file branch 5 times, most recently from b54dae9 to 324b2a2 Compare November 25, 2019 10:19
@rlefevre
Copy link
Contributor Author

rlefevre commented Nov 25, 2019

To avoid unexpected behavior changes:

  • Per file detection is used only:
    • when elm-format does not run with --upgrade
    • when there is no --elm-version flag
  • Auto-detection from the directory where elm-format is run is still used to determine version with --stdin if no --elm-version flag is provided.

@rlefevre rlefevre marked this pull request as ready for review November 25, 2019 10:24
src/ElmFormat.hs Outdated

type ElmFile
Copy link
Contributor Author

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?

Copy link
Owner

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.

Copy link
Contributor Author

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.hs Show resolved Hide resolved
@@ -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)
Copy link
Contributor Author

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.

Copy link
Owner

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.

Copy link
Owner

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.

Copy link
Contributor Author

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.

src/ElmVersion.hs Outdated Show resolved Hide resolved
@rlefevre
Copy link
Contributor Author

The following instructions from README.md should also be updated:

You must run elm-format from the directory that contains your elm.json (for Elm 0.19) or elm-package.json (for Elm 0.18), or else you must pass the appropriate --elm-version=0.19/--elm-version=0.18 command line argument.

Maybe you have a suggestion or this can be done later when preparing the release?

@rlefevre rlefevre force-pushed the autodetect-elm-version-per-file branch from 324b2a2 to 15f3458 Compare November 26, 2019 00:28
@rlefevre rlefevre changed the title Autodetect elm version per file Auto-detect elm version per file Nov 26, 2019
@avh4
Copy link
Owner

avh4 commented Nov 27, 2019

The following instructions from README.md should also be updated

I guess if we're not doing that now (because we want master's README to still describe the existing version, could you maybe make a new github issue to track that, and I'll put it at the end of the next milestone

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 "."
Copy link
Owner

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.

Copy link
Contributor Author

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 =
Copy link
Owner

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?

Copy link
Contributor Author

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.

_ -> Elm_0_18

_ | path == dir ->
return Elm_0_19
Copy link
Owner

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?

Copy link
Owner

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in last version.

do
let dir = takeDirectory (path)
elmPackageJson <- stat (path </> "elm-package.json")
case elmPackageJson of
Copy link
Owner

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.

Copy link
Contributor Author

@rlefevre rlefevre Nov 27, 2019

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 surprisingly takeDirectory ".." == "." (and takeDirectory "../.." == "..") 🤔

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 or elm.json upward up to /, trying to detect an upward version
    • then if it is a directory, continue recursively downward to search elm files, detecting elm-package.json and elm.json on the way, and therefore downward versions (in an optimized way), once per directory.

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
Copy link
Owner

Choose a reason for hiding this comment

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

👍

@avh4
Copy link
Owner

avh4 commented Nov 27, 2019

Looks like a great start, thanks!

@avh4 avh4 added this to the 0.8.4 milestone Nov 27, 2019
@avh4
Copy link
Owner

avh4 commented Nov 28, 2019

Ah, well my comments were based on the assumption that normalise was getting to an absolute path. I'm not actually sure what's necessary...

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 cd into a subfolder and want to run elm-format just on files under that folder. I think that would require going up through the actual filesystem and not just removing segments from the path. Is that a case we want to support? As for CI use, I don't think elm-format would typically be run from the project folder or from the folder where the elm.json is.

@rlefevre rlefevre force-pushed the autodetect-elm-version-per-file branch from 15f3458 to 3b2edf7 Compare December 2, 2019 08:12
@rlefevre
Copy link
Contributor Author

rlefevre commented Dec 2, 2019

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 --stdin and without --elm-version.

@rlefevre rlefevre force-pushed the autodetect-elm-version-per-file branch from 3b2edf7 to 8addbf1 Compare December 2, 2019 08:21
@@ -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 "."
Copy link
Contributor Author

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)
Copy link
Contributor Author

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)
Copy link
Contributor Author

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 ++ ")"
Copy link
Contributor Author

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
Copy link
Contributor Author

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.

do
source <- determineSource (Flags._stdin config) resolvedInputFiles
checkUpgradeVersion (Flags._upgrade config) (Flags._elmVersion config)
Copy link
Contributor Author

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.

@rlefevre rlefevre force-pushed the autodetect-elm-version-per-file branch from 244172d to c09e060 Compare December 2, 2019 09:50
(True, Elm_0_18) ->
Elm_0_18_Upgrade

(True, _) ->
Copy link
Contributor Author

@rlefevre rlefevre Dec 2, 2019

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
Copy link
Contributor Author

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 🤔

@avh4 avh4 modified the milestones: 0.8.4, 0.8.5 Sep 14, 2020
@avh4 avh4 changed the base branch from master to main September 18, 2020 02:54
@avh4 avh4 modified the milestones: 0.8.5, 0.8.6 Feb 4, 2021
@avh4 avh4 modified the milestones: 0.8.6, Future 0.8.* Jan 26, 2023
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.

auto-detect elm version per file, instead of based on cwd
2 participants