Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions src/Nixfmt/Parser.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -75,7 +75,7 @@ import Text.Megaparsec (
(<|>),
)
import Text.Megaparsec.Char (char, digitChar)
import Prelude hiding (String)
import Prelude hiding (String, elem)

-- HELPER FUNCTIONS

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions standard.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions test/correct/indented-string.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
$'\t'
''

''ending dollar $''
''$''
''"ending dollar $''
''"$''
]
1 change: 1 addition & 0 deletions test/diff/idioms_nixos_2/in.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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.'';
}
]; }
Expand Down
3 changes: 2 additions & 1 deletion test/diff/idioms_nixos_2/out-pure.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
}
];
}
Expand Down
3 changes: 2 additions & 1 deletion test/diff/idioms_nixos_2/out.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
}
];
}
Expand Down
6 changes: 6 additions & 0 deletions test/diff/string/in.nix
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,10 @@
"--${
"test"
}"
''''${pkgs.ghostscript}/bin/ps2pdf''
'''test''$test''
'''test'''quotes''
'''plain''
'' between spaces ''
'' !#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~''
]
18 changes: 12 additions & 6 deletions test/diff/string/out-pure.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
b
"
###
''''
""
###
''a''
"a"
###
''${""}''
"${""}"
###
''
${""}
Expand Down Expand Up @@ -51,7 +51,7 @@
e
''
###
''''
""
###
''
declare -a makefiles=(./*.mak)
Expand All @@ -66,15 +66,15 @@
[${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: ' ''
###
''exec i3-input -F '[con_mark="%s"] focus' -l 1 -P 'Go to: ' ''
###
''"${pkgs.name or "<unknown-name>"}";''
###
''${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/"
Expand All @@ -92,4 +92,10 @@
''

"--${"test"}"
"\${pkgs.ghostscript}/bin/ps2pdf"
"'test\$test"
"'test''quotes"
"'plain"
"between spaces "
"!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~"
]
18 changes: 12 additions & 6 deletions test/diff/string/out.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
b
"
###
''''
""
###
''a''
"a"
###
''${""}''
"${""}"
###
''
${""}
Expand Down Expand Up @@ -51,7 +51,7 @@
e
''
###
''''
""
###
''
declare -a makefiles=(./*.mak)
Expand All @@ -66,15 +66,15 @@
[${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: ' ''
###
''exec i3-input -F '[con_mark="%s"] focus' -l 1 -P 'Go to: ' ''
###
''"${pkgs.name or "<unknown-name>"}";''
###
''${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/"
Expand All @@ -92,4 +92,10 @@
''

"--${"test"}"
"\${pkgs.ghostscript}/bin/ps2pdf"
"'test\$test"
"'test''quotes"
"'plain"
"between spaces "
"!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~"
]
6 changes: 3 additions & 3 deletions test/diff/string_interpol/out-pure.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions test/diff/string_interpol/out.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down