diff --git a/CHANGELOG.md b/CHANGELOG.md index 829faf11ffa..2c5a401cbb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ #### :boom: Breaking Change +- `\"_"` is no longer allowed as a quoted identifier, as it couldn't be told apart from the `_` wildcard and placeholder. https://github.com/rescript-lang/rescript/pull/8795 - Record syntax no longer creates or matches dicts, e.g. `let d: dict = {foo: 1}`; it's an error pointing to `dict{...}`. https://github.com/rescript-lang/rescript/pull/8751 - Remove the call-site `@inlined` attribute, which was parsed but never affected code generation. It is now reported as a misplaced attribute (warning 53). https://github.com/rescript-lang/rescript/pull/8734 - Remove `@deriving(abstract)` and `@deriving(jsConverter)`. Use record types (with optional fields, mutable fields and `@as` renaming) and polymorphic variants directly instead. https://github.com/rescript-lang/rescript/pull/8729 @@ -30,6 +31,7 @@ - Fix parser recovery and editor completion when a record or dict value is missing before a comma, e.g. `{a: , b: 1}`. https://github.com/rescript-lang/rescript/pull/8779 - Derive module names from file names consistently, fixing files like `Button.test.res` that could not reference `Button`, and `.res`/`.resi` pairs with dotted or non-ASCII names. https://github.com/rescript-lang/rescript/pull/8797 +- Fix the formatter printing escaped identifiers starting with `_`, like `\"_'"`, in a form that doesn't parse. https://github.com/rescript-lang/rescript/pull/8795 - Fix references to a `@val` global reading a binding with the same name, e.g. a toplevel `let` declared before them. https://github.com/rescript-lang/rescript/pull/8785 - Evaluate the unused arguments of an inlined function call in parameter order. https://github.com/rescript-lang/rescript/pull/8786 - Fix shadowed variables in generated JS colliding with identifiers or external names containing `$`. https://github.com/rescript-lang/rescript/pull/8778 diff --git a/analysis/src/utils.ml b/analysis/src/utils.ml index 6b2b06624f0..68734918570 100644 --- a/analysis/src/utils.ml +++ b/analysis/src/utils.ml @@ -250,7 +250,13 @@ let print_maybe_exotic_ident ?(allow_uident = false) txt = else if i == 0 then match String.unsafe_get txt i with | 'A' .. 'Z' when allow_uident -> loop (i + 1) - | 'a' .. 'z' | '_' | '$' -> loop (i + 1) + | 'a' .. 'z' | '$' -> loop (i + 1) + (* Same as Res_printer.classify_ident_content: a lone [_] or one + followed by another character is not scanned as an identifier. *) + | '_' when len > 1 -> ( + match String.unsafe_get txt 1 with + | 'A' .. 'Z' | 'a' .. 'z' | '0' .. '9' | '_' | '$' -> loop (i + 1) + | _ -> "\"" ^ txt ^ "\"") | _ -> "\"" ^ txt ^ "\"" else match String.unsafe_get txt i with diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 2ca0632937e..401e16542b9 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -496,7 +496,13 @@ let classify_ident_content ?(allow_uident = false) ?(allow_hyphen = false) txt = match String.unsafe_get txt i with | '\\' -> UppercaseExoticIdent | 'A' .. 'Z' when allow_uident -> loop (i + 1) - | 'a' .. 'z' | '_' | '$' -> loop (i + 1) + | 'a' .. 'z' | '$' -> loop (i + 1) + (* The scanner reads [_] as an identifier only when one of these + follows; otherwise it is a wildcard. *) + | '_' when len > 1 -> ( + match String.unsafe_get txt 1 with + | 'A' .. 'Z' | 'a' .. 'z' | '0' .. '9' | '_' | '$' -> loop (i + 1) + | _ -> ExoticIdent) | _ -> ExoticIdent else match String.unsafe_get txt i with @@ -3338,6 +3344,10 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = let constr = print_longident_location longident_loc cmt_tbl in let args = print_expression_args ~state args cmt_tbl in Doc.group (Doc.concat [constr; args]) + | Pexp_ident ({txt = Longident.Lident "_"} as path) -> + (* The `_` placeholder of underscore apply sugar, see + [Parsetree_viewer.rewrite_underscore_apply]. *) + print_comments (Doc.text "_") cmt_tbl path.loc | Pexp_ident path -> print_lident_path path cmt_tbl | Pexp_tuple exprs -> Doc.group diff --git a/compiler/syntax/src/res_scanner.ml b/compiler/syntax/src/res_scanner.ml index 3f1ea89a0ce..adcec8e6589 100644 --- a/compiler/syntax/src/res_scanner.ml +++ b/compiler/syntax/src/res_scanner.ml @@ -331,6 +331,13 @@ let scan_exotic_identifier scanner = scanner.err ~start_pos ~end_pos (Diagnostics.message "A quoted identifier can't be empty string."); Token.Lident ident) + else if name = "_" then ( + (* `_` is the wildcard and the placeholder of underscore apply sugar; + a quoted `_` couldn't be told apart from them. *) + let end_pos = position scanner in + scanner.err ~start_pos ~end_pos + (Diagnostics.message "`_` can't be used as a quoted identifier."); + Token.Lident name) else if Ext_ident.is_uident name then Token.Lident ident (* Exotic ident with uppercase letter should be encoded to avoid confusing in OCaml parsetree *) else Token.Lident name diff --git a/tests/analysis_tests/tests/src/CompletionExpressions.res b/tests/analysis_tests/tests/src/CompletionExpressions.res index 80c29875d59..a61796e5e5e 100644 --- a/tests/analysis_tests/tests/src/CompletionExpressions.res +++ b/tests/analysis_tests/tests/src/CompletionExpressions.res @@ -263,7 +263,7 @@ let arr = ["hello"] // arr->Belt.Array.map() // ^com -type exoticPolyvariant = [#"some exotic" | #dollar$] +type exoticPolyvariant = [#"some exotic" | #dollar$ | #"_"] let takesExotic = (e: exoticPolyvariant) => { ignore(e) diff --git a/tests/analysis_tests/tests/src/expected/CompletionExpressions.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExpressions.res.txt index b21d96f613c..7889521e2bd 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionExpressions.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionExpressions.res.txt @@ -1330,7 +1330,7 @@ Path takesExotic "detail": "#\"some exotic\"", "documentation": { "kind": "markdown", - "value": "```rescript\n#\"some exotic\"\n```\n\n```rescript\n[#dollar$ | #\"some exotic\"]\n```" + "value": "```rescript\n#\"some exotic\"\n```\n\n```rescript\n[#\"_\" | #dollar$ | #\"some exotic\"]\n```" }, "insertText": "#\"some exotic\"", "insertTextFormat": 2, @@ -1338,11 +1338,23 @@ Path takesExotic "label": "#\"some exotic\"", "tags": [] }, + { + "detail": "#\"_\"", + "documentation": { + "kind": "markdown", + "value": "```rescript\n#\"_\"\n```\n\n```rescript\n[#\"_\" | #dollar$ | #\"some exotic\"]\n```" + }, + "insertText": "#\"_\"", + "insertTextFormat": 2, + "kind": 4, + "label": "#\"_\"", + "tags": [] + }, { "detail": "#dollar$", "documentation": { "kind": "markdown", - "value": "```rescript\n#dollar$\n```\n\n```rescript\n[#dollar$ | #\"some exotic\"]\n```" + "value": "```rescript\n#dollar$\n```\n\n```rescript\n[#\"_\" | #dollar$ | #\"some exotic\"]\n```" }, "insertText": "#dollar$", "insertTextFormat": 2, diff --git a/tests/syntax_tests/data/parsing/errors/scanner/expected/quotedUnderscore.res.txt b/tests/syntax_tests/data/parsing/errors/scanner/expected/quotedUnderscore.res.txt new file mode 100644 index 00000000000..531f2a73c7b --- /dev/null +++ b/tests/syntax_tests/data/parsing/errors/scanner/expected/quotedUnderscore.res.txt @@ -0,0 +1,22 @@ + + Syntax error! + syntax_tests/data/parsing/errors/scanner/quotedUnderscore.res:1:5-8 + + 1 │ let \"_" = 1 + 2 │ let x = f(\"_") + 3 │ + + `_` can't be used as a quoted identifier. + + + Syntax error! + syntax_tests/data/parsing/errors/scanner/quotedUnderscore.res:2:11-14 + + 1 │ let \"_" = 1 + 2 │ let x = f(\"_") + 3 │ + + `_` can't be used as a quoted identifier. + +let _ = 1 +let x [arity:1]__x = f __x \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/errors/scanner/quotedUnderscore.res b/tests/syntax_tests/data/parsing/errors/scanner/quotedUnderscore.res new file mode 100644 index 00000000000..03d0b1d127c --- /dev/null +++ b/tests/syntax_tests/data/parsing/errors/scanner/quotedUnderscore.res @@ -0,0 +1,2 @@ +let \"_" = 1 +let x = f(\"_") diff --git a/tests/syntax_tests/data/printer/expr/expected/underscoreExoticIdent.res.txt b/tests/syntax_tests/data/printer/expr/expected/underscoreExoticIdent.res.txt new file mode 100644 index 00000000000..23afb557fa3 --- /dev/null +++ b/tests/syntax_tests/data/printer/expr/expected/underscoreExoticIdent.res.txt @@ -0,0 +1,11 @@ +let \"_'" = 2 +let \"_''" = 3 +let __' = 4 +let _a' = 5 +let f = (~\"_'") => \"_'" +let a = #"_'" +let b = #"_" +type r = {\"_'": int} +let r = {\"_'": 1} +let g = r.\"_'" +let x = f(a, _, c) diff --git a/tests/syntax_tests/data/printer/expr/underscoreExoticIdent.res b/tests/syntax_tests/data/printer/expr/underscoreExoticIdent.res new file mode 100644 index 00000000000..23afb557fa3 --- /dev/null +++ b/tests/syntax_tests/data/printer/expr/underscoreExoticIdent.res @@ -0,0 +1,11 @@ +let \"_'" = 2 +let \"_''" = 3 +let __' = 4 +let _a' = 5 +let f = (~\"_'") => \"_'" +let a = #"_'" +let b = #"_" +type r = {\"_'": int} +let r = {\"_'": 1} +let g = r.\"_'" +let x = f(a, _, c)