diff --git a/src/Nixfmt/Parser.hs b/src/Nixfmt/Parser.hs index 20a7baea..12b4f1ed 100644 --- a/src/Nixfmt/Parser.hs +++ b/src/Nixfmt/Parser.hs @@ -17,7 +17,7 @@ import Data.Char (isAlpha) import Data.Foldable (toList) import Data.Functor (($>)) import Data.Maybe (fromMaybe, mapMaybe, maybeToList) -import Data.Text (Text, pack) +import Data.Text (Text, elem, isPrefixOf, pack) import qualified Data.Text as Text import Data.Void (Void) import Nixfmt.Lexer (lexeme, takeTrivia, whole) @@ -75,7 +75,7 @@ import Text.Megaparsec ( (<|>), ) import Text.Megaparsec.Char (char, digitChar) -import Prelude hiding (String) +import Prelude hiding (String, elem) -- HELPER FUNCTIONS @@ -324,6 +324,39 @@ indentedString = *> fmap fixIndentedString (sepBy indentedLine (chunk "\n")) <* rawSymbol TDoubleSingleQuote +-- | Parser for all string types (simple, URI, or indented) +string :: Parser Term +string = + (SimpleString <$> lexeme (simpleString <|> uri)) + <|> (classifyString <$> lexeme indentedString) + where + -- Converts indented string syntax to appropriate string type. + -- If the content can be represented as a simple string (no newlines, quotes or backslashes), + -- it's reformatted as SimpleString to maintain a consistent style. + classifyString s + | shouldBeSimpleString (value s) = SimpleString (s{value = convertIndentedEscapes (value s)}) + | otherwise = IndentedString s + + shouldBeSimpleString parts = + not (containsNewlines parts) && not (any (any hasQuoteOrBackSlash) parts) + + containsNewlines parts = length parts > 1 + + hasQuoteOrBackSlash (TextPart t) = '"' `elem` t || '\\' `elem` t + hasQuoteOrBackSlash (Interpolation _) = False + + convertIndentedEscapes = map $ map convertPart + + convertPart (TextPart t) = TextPart (convertEscapes t) + convertPart (Interpolation x) = Interpolation x + + -- Converts indented string escapes to simple string escapes + convertEscapes t + | Text.null t = t + | "''$" `isPrefixOf` t = "\\$" <> convertEscapes (Text.drop 3 t) -- ''$ -> \$ + | "'''" `isPrefixOf` t = "''" <> convertEscapes (Text.drop 3 t) -- ''' -> '' + | otherwise = Text.take 1 t <> convertEscapes (Text.drop 1 t) + -- TERMS parens :: Parser Term @@ -358,8 +391,7 @@ selectorPath' = many $ try $ selector $ Just $ symbol TDot -- Everything but selection simpleTerm :: Parser Term simpleTerm = - (SimpleString <$> lexeme (simpleString <|> uri)) - <|> (IndentedString <$> lexeme indentedString) + string <|> (Path <$> path) <|> (Token <$> (envPath <|> float <|> integer <|> identifier)) <|> parens diff --git a/standard.md b/standard.md index f866431d..ac390ecc 100644 --- a/standard.md +++ b/standard.md @@ -327,6 +327,10 @@ For list elements, attributes, and function arguments, the following applies: ### Strings - The kind of quotes used in strings (`"` vs `''`) must be preserved from the input. + - Exception: Indented strings (`''...''`) that contain no newlines, double quote characters or backslashes are automatically reformatted as simple strings (`"..."`). + - When converting indented strings to simple strings, escape sequences are rewritten to maintain semantic equivalence: + - `''$` becomes `\$` (escaped dollar sign) + - `'''` becomes `''` (quotes) - The non-interpolated string parts must be preserved from the input - E.g. changing `\t` to a tab character must not be done automatically @@ -343,6 +347,22 @@ For list elements, attributes, and function arguments, the following applies: '' This is a really long string that would not fit within the line length limit '' + +# Indented strings with simple content get reformatted as simple strings +''hello'' # becomes "hello" +''hello world'' # becomes "hello world" + +# Escape sequences are rewritten when converting to simple strings +''''${pkgs.ghostscript}/bin/ps2pdf'' # becomes "\${pkgs.ghostscript}/bin/ps2pdf" +'''test''$var'' # becomes "'test\$var" +'''can''t''' # becomes "'can't" + +# But these stay as indented strings +'' + hello + world +'' +''hello "quoted" text'' ``` #### Interpolations diff --git a/test/correct/indented-string.nix b/test/correct/indented-string.nix index 0e7eb5a7..61e30fdb 100644 --- a/test/correct/indented-string.nix +++ b/test/correct/indented-string.nix @@ -9,6 +9,6 @@ $'\t' '' - ''ending dollar $'' - ''$'' + ''"ending dollar $'' + ''"$'' ] diff --git a/test/diff/idioms_nixos_2/in.nix b/test/diff/idioms_nixos_2/in.nix index 76a01727..e0713bab 100644 --- a/test/diff/idioms_nixos_2/in.nix +++ b/test/diff/idioms_nixos_2/in.nix @@ -734,6 +734,7 @@ in { { assertions = [ { assertion = cfg.database.createLocally -> cfg.config.dbtype == "mysql"; + # single line idented strings must be reformatted to simple strings message = ''services.nextcloud.config.dbtype must be set to mysql if services.nextcloud.database.createLocally is set to true.''; } ]; } diff --git a/test/diff/idioms_nixos_2/out-pure.nix b/test/diff/idioms_nixos_2/out-pure.nix index 20419b3c..a9783291 100644 --- a/test/diff/idioms_nixos_2/out-pure.nix +++ b/test/diff/idioms_nixos_2/out-pure.nix @@ -794,7 +794,8 @@ in assertions = [ { assertion = cfg.database.createLocally -> cfg.config.dbtype == "mysql"; - message = ''services.nextcloud.config.dbtype must be set to mysql if services.nextcloud.database.createLocally is set to true.''; + # single line idented strings must be reformatted to simple strings + message = "services.nextcloud.config.dbtype must be set to mysql if services.nextcloud.database.createLocally is set to true."; } ]; } diff --git a/test/diff/idioms_nixos_2/out.nix b/test/diff/idioms_nixos_2/out.nix index bfdb9c97..0534189c 100644 --- a/test/diff/idioms_nixos_2/out.nix +++ b/test/diff/idioms_nixos_2/out.nix @@ -796,7 +796,8 @@ in assertions = [ { assertion = cfg.database.createLocally -> cfg.config.dbtype == "mysql"; - message = ''services.nextcloud.config.dbtype must be set to mysql if services.nextcloud.database.createLocally is set to true.''; + # single line idented strings must be reformatted to simple strings + message = "services.nextcloud.config.dbtype must be set to mysql if services.nextcloud.database.createLocally is set to true."; } ]; } diff --git a/test/diff/string/in.nix b/test/diff/string/in.nix index 177facaa..2a9aaa66 100644 --- a/test/diff/string/in.nix +++ b/test/diff/string/in.nix @@ -92,4 +92,10 @@ "--${ "test" }" + ''''${pkgs.ghostscript}/bin/ps2pdf'' + '''test''$test'' + '''test'''quotes'' + '''plain'' + '' between spaces '' + '' !#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~'' ] diff --git a/test/diff/string/out-pure.nix b/test/diff/string/out-pure.nix index f5d1ebca..a8e8ac1e 100644 --- a/test/diff/string/out-pure.nix +++ b/test/diff/string/out-pure.nix @@ -13,11 +13,11 @@ b " ### - '''' + "" ### - ''a'' + "a" ### - ''${""}'' + "${""}" ### '' ${""} @@ -51,7 +51,7 @@ e '' ### - '''' + "" ### '' declare -a makefiles=(./*.mak) @@ -66,7 +66,7 @@ [${mkSectionName sectName}] '' ### - ''-couch_ini ${cfg.package}/etc/default.ini ${configFile} ${pkgs.writeText "couchdb-extra.ini" cfg.extraConfig} ${cfg.configFile}'' + "-couch_ini ${cfg.package}/etc/default.ini ${configFile} ${pkgs.writeText "couchdb-extra.ini" cfg.extraConfig} ${cfg.configFile}" ### ''exec i3-input -F "mark %s" -l 1 -P 'Mark: ' '' ### @@ -74,7 +74,7 @@ ### ''"${pkgs.name or ""}";'' ### - ''${pkgs.replace-secret}/bin/replace-secret '${placeholder}' '${secretFile}' '${targetFile}' '' + "${pkgs.replace-secret}/bin/replace-secret '${placeholder}' '${secretFile}' '${targetFile}' " ### '' mkdir -p "$out/lib/modules/${kernel.modDirVersion}/kernel/net/wireless/" @@ -92,4 +92,10 @@ '' "--${"test"}" + "\${pkgs.ghostscript}/bin/ps2pdf" + "'test\$test" + "'test''quotes" + "'plain" + "between spaces " + "!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~" ] diff --git a/test/diff/string/out.nix b/test/diff/string/out.nix index f5d1ebca..a8e8ac1e 100644 --- a/test/diff/string/out.nix +++ b/test/diff/string/out.nix @@ -13,11 +13,11 @@ b " ### - '''' + "" ### - ''a'' + "a" ### - ''${""}'' + "${""}" ### '' ${""} @@ -51,7 +51,7 @@ e '' ### - '''' + "" ### '' declare -a makefiles=(./*.mak) @@ -66,7 +66,7 @@ [${mkSectionName sectName}] '' ### - ''-couch_ini ${cfg.package}/etc/default.ini ${configFile} ${pkgs.writeText "couchdb-extra.ini" cfg.extraConfig} ${cfg.configFile}'' + "-couch_ini ${cfg.package}/etc/default.ini ${configFile} ${pkgs.writeText "couchdb-extra.ini" cfg.extraConfig} ${cfg.configFile}" ### ''exec i3-input -F "mark %s" -l 1 -P 'Mark: ' '' ### @@ -74,7 +74,7 @@ ### ''"${pkgs.name or ""}";'' ### - ''${pkgs.replace-secret}/bin/replace-secret '${placeholder}' '${secretFile}' '${targetFile}' '' + "${pkgs.replace-secret}/bin/replace-secret '${placeholder}' '${secretFile}' '${targetFile}' " ### '' mkdir -p "$out/lib/modules/${kernel.modDirVersion}/kernel/net/wireless/" @@ -92,4 +92,10 @@ '' "--${"test"}" + "\${pkgs.ghostscript}/bin/ps2pdf" + "'test\$test" + "'test''quotes" + "'plain" + "between spaces " + "!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~" ] diff --git a/test/diff/string_interpol/out-pure.nix b/test/diff/string_interpol/out-pure.nix index e92a237d..0910499f 100644 --- a/test/diff/string_interpol/out-pure.nix +++ b/test/diff/string_interpol/out-pure.nix @@ -2,9 +2,9 @@ "${ /* a */ "${/* b */ "${c}"}" # d }" - ''${ - /* a */ ''${/* b */ ''${c}''}'' # d - }'' + "${ + /* a */ "${/* b */ "${c}"}" # d + }" { ExecStart = "${pkgs.openarena}/bin/oa_ded +set fs_basepath ${pkgs.openarena}/openarena-0.8.8 +set fs_homepath /var/lib/openarena ${ concatMapStringsSep (x: x) " " cfg.extraFlags diff --git a/test/diff/string_interpol/out.nix b/test/diff/string_interpol/out.nix index e92a237d..0910499f 100644 --- a/test/diff/string_interpol/out.nix +++ b/test/diff/string_interpol/out.nix @@ -2,9 +2,9 @@ "${ /* a */ "${/* b */ "${c}"}" # d }" - ''${ - /* a */ ''${/* b */ ''${c}''}'' # d - }'' + "${ + /* a */ "${/* b */ "${c}"}" # d + }" { ExecStart = "${pkgs.openarena}/bin/oa_ded +set fs_basepath ${pkgs.openarena}/openarena-0.8.8 +set fs_homepath /var/lib/openarena ${ concatMapStringsSep (x: x) " " cfg.extraFlags