diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fb66b00fd2..4641435c5bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ #### :nail_care: Polish - Allow builds while watchers are running. https://github.com/rescript-lang/rescript/pull/8349 +- Restore parsing of the legacy `(. ...)` uncurried syntax for backwards compatibility with libraries still on older ReScript versions; emit a deprecation warning when it is used. Rewatch also surfaces this specific deprecation when it originates from an external dependency so users can report breakage upstream. https://github.com/rescript-lang/rescript/pull/8383 - Rewatch: replace wave-based compile scheduler with a work-stealing DAG dispatcher ordered by critical-path priority, avoiding the per-wave stall on the slowest file. https://github.com/rescript-lang/rescript/pull/8374 #### :house: Internal diff --git a/compiler/syntax/src/res_core.ml b/compiler/syntax/src/res_core.ml index 349b930fd2f..619f71e6929 100644 --- a/compiler/syntax/src/res_core.ml +++ b/compiler/syntax/src/res_core.ml @@ -285,6 +285,19 @@ let tagged_template_literal_attr = let spread_attr = (Location.mknoloc "res.spread", Parsetree.PStr []) let dict_spread_attr = (Location.mknoloc "res.dictSpread", Parsetree.PStr []) +(* Emit a deprecation warning when the legacy [(. ...)] uncurried syntax is + encountered. Uncurried is the default since ReScript v11, so the leading + dot is no longer meaningful; we still accept it so dependencies on older + libraries keep parsing. *) +let warn_uncurried_dot_syntax ~loc = + Location.prerr_warning loc + (Warnings.Deprecated + ( "The `(. ...)` uncurried syntax is deprecated. Uncurried is now the \ + default in ReScript — remove the leading dot.", + loc, + loc, + false )) + type argument = {label: Asttypes.arg_label; expr: Parsetree.expression} type type_parameter = { @@ -1857,6 +1870,9 @@ and parse_es6_arrow_expression ?(arrow_attrs = []) ?(arrow_start_pos = None) {arrow_expr with pexp_loc = {arrow_expr.pexp_loc with loc_start = start_pos}} (* + * dotted_parameter ::= + * | . parameter (* deprecated uncurried syntax *) + * * parameter ::= * | pattern * | pattern : type @@ -1874,10 +1890,14 @@ and parse_es6_arrow_expression ?(arrow_attrs = []) ?(arrow_start_pos = None) *) and parse_parameter p = if - p.Parser.token = Token.Typ || p.token = Tilde + p.Parser.token = Token.Typ || p.token = Tilde || p.token = Dot || Grammar.is_pattern_start p.token - then + then ( let start_pos = p.Parser.start_pos in + if p.Parser.token = Token.Dot then ( + let dot_loc = mk_loc start_pos p.end_pos in + Parser.next p; + warn_uncurried_dot_syntax ~loc:dot_loc); let attrs = parse_attributes p in if p.Parser.token = Typ then ( Parser.next p; @@ -1962,7 +1982,7 @@ and parse_parameter p = | _ -> Some (TermParameter - {attrs; p_label = lbl; expr = None; pat; p_pos = start_pos}) + {attrs; p_label = lbl; expr = None; pat; p_pos = start_pos})) else None and parse_parameter_list p = @@ -1977,6 +1997,7 @@ and parse_parameter_list p = * | _ * | lident * | () + * | (.) (* deprecated uncurried syntax *) * | ( parameter {, parameter} [,] ) *) and parse_parameters p : fundef_type_param option * fundef_term_param list = @@ -2025,6 +2046,10 @@ and parse_parameters p : fundef_type_param option * fundef_term_param list = ] ) | Lparen -> Parser.next p; + if p.Parser.token = Token.Dot then ( + let dot_loc = mk_loc p.start_pos p.end_pos in + Parser.next p; + warn_uncurried_dot_syntax ~loc:dot_loc); let type_params, term_params = parse_parameter_list p in let term_params = if term_params <> [] then term_params else [unit_term_parameter ()] @@ -4033,13 +4058,31 @@ and parse_switch_expression p = * | ~ label-name = ? _ (* syntax sugar *) * | ~ label-name = ? expr : type * + * dotted_argument ::= + * | . argument (* deprecated uncurried syntax *) *) and parse_argument p : argument option = if p.Parser.token = Token.Tilde - || p.token = Underscore + || p.token = Dot || p.token = Underscore || Grammar.is_expr_start p.token - then parse_argument2 p + then + match p.Parser.token with + | Dot -> ( + let dot_loc = mk_loc p.start_pos p.end_pos in + Parser.next p; + warn_uncurried_dot_syntax ~loc:dot_loc; + match p.token with + (* apply(.) — legacy uncurried unit call *) + | Rparen -> + let unit_expr = + Ast_helper.Exp.construct + (Location.mknoloc (Longident.Lident "()")) + None + in + Some {label = Asttypes.Nolabel; expr = unit_expr} + | _ -> parse_argument2 p) + | _ -> parse_argument2 p else None and parse_argument2 p : argument option = @@ -4796,6 +4839,9 @@ and parse_type_alias p typ = * note: * | attrs ~ident: type_expr -> attrs are on the arrow * | attrs type_expr -> attrs are here part of the type_expr + * + * dotted_type_parameter ::= + * | . type_parameter (* deprecated uncurried syntax *) *) and parse_type_parameter ?current_type_name_path ?inline_types_context ?positional_type_name_path p = @@ -4806,8 +4852,16 @@ and parse_type_parameter ?current_type_name_path ?inline_types_context [doc_comment_to_attribute loc s] | _ -> [] in - if p.Parser.token = Token.Tilde || Grammar.is_typ_expr_start p.token then + if + p.Parser.token = Token.Tilde + || p.token = Dot + || Grammar.is_typ_expr_start p.token + then ( let start_pos = p.Parser.start_pos in + if p.Parser.token = Token.Dot then ( + let dot_loc = mk_loc start_pos p.end_pos in + Parser.next p; + warn_uncurried_dot_syntax ~loc:dot_loc); let attrs = doc_attr @ parse_attributes p in match p.Parser.token with | Tilde -> ( @@ -4879,7 +4933,7 @@ and parse_type_parameter ?current_type_name_path ?inline_types_context let typ_with_attributes = {typ with ptyp_attributes = List.concat [attrs; typ.ptyp_attributes]} in - Some {attrs = []; label = Nolabel; typ = typ_with_attributes; start_pos} + Some {attrs = []; label = Nolabel; typ = typ_with_attributes; start_pos}) else None (* (int, ~x:string, float) *) diff --git a/compiler/syntax/src/res_grammar.ml b/compiler/syntax/src/res_grammar.ml index 1ab5bd48881..de4a9578064 100644 --- a/compiler/syntax/src/res_grammar.ml +++ b/compiler/syntax/src/res_grammar.ml @@ -178,7 +178,7 @@ let is_pattern_start = function | _ -> false let is_parameter_start = function - | Token.Typ | Tilde -> true + | Token.Typ | Tilde | Dot -> true | token when is_pattern_start token -> true | _ -> false @@ -206,7 +206,7 @@ let is_typ_expr_start = function | _ -> false let is_type_parameter_start = function - | Token.Tilde -> true + | Token.Tilde | Dot -> true | token when is_typ_expr_start token -> true | _ -> false @@ -239,7 +239,7 @@ let is_record_row_string_key_start = function | _ -> false let is_argument_start = function - | Token.Tilde | Underscore -> true + | Token.Tilde | Dot | Underscore -> true | t when is_expr_start t -> true | _ -> false diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index 2a434ac7ee5..84683cdac32 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -1095,10 +1095,13 @@ fn compile_file( if helpers::contains_ascii_characters(&err) { if package.is_local_dep { - // suppress warnings of external deps Ok(Some(err)) } else { - Ok(None) + // Warnings from external deps are suppressed by default — + // users can't act on them. A small allow-list of critical + // deprecations still gets through so breakage signals are + // visible (and can be reported upstream). + Ok(retain_critical_external_warnings(&err)) } } else { Ok(None) @@ -1107,6 +1110,35 @@ fn compile_file( } } +/// Filter a bsc stderr capture to the warning blocks the user needs to see +/// even when they originate in an external dependency. +/// +/// Currently preserved: +/// - Warning 3 deprecations mentioning the legacy `(. ...)` uncurried syntax. +/// These indicate source that parses today but is scheduled for removal, so +/// consumers need to hear about them even when the code isn't theirs. +pub(super) fn retain_critical_external_warnings(stderr: &str) -> Option { + const UNCURRIED_DOT_MARKER: &str = "`(. ...)` uncurried syntax"; + if !stderr.contains(UNCURRIED_DOT_MARKER) { + return None; + } + // bsc prints each warning as its own block separated by a blank-line pair + // (three consecutive newlines). On Windows the same stream uses CRLF, so + // normalize before splitting to keep the block boundary recognizable — + // otherwise the whole stderr would be treated as a single block and + // unrelated warnings would leak through alongside the critical one. + let normalized = stderr.replace("\r\n", "\n"); + let kept: Vec<&str> = normalized + .split("\n\n\n") + .filter(|block| block.contains(UNCURRIED_DOT_MARKER)) + .collect(); + if kept.is_empty() { + None + } else { + Some(kept.join("\n\n\n")) + } +} + pub fn mark_modules_with_deleted_deps_dirty(build_state: &mut BuildState) { build_state.modules.iter_mut().for_each(|(_, module)| { if !module.deps.is_disjoint(&build_state.deleted_modules) { @@ -1203,3 +1235,42 @@ pub fn mark_modules_with_expired_deps_dirty(build_state: &mut BuildCommandState) } }); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn retain_critical_external_warnings_returns_none_without_marker() { + let input = "\n Warning number 26\n foo.res:1:1\n\n unused variable x.\n"; + assert_eq!(retain_critical_external_warnings(input), None); + } + + #[test] + fn retain_critical_external_warnings_keeps_uncurried_dot_block() { + let input = concat!( + "\n Warning number 26\n foo.res:1:1\n\n unused variable x.\n", + "\n\n\n Warning number 3\n bar.res:5:10\n\n ", + "deprecated: The `(. ...)` uncurried syntax is deprecated.\n", + ); + let kept = retain_critical_external_warnings(input).expect("uncurried-dot warning should survive"); + assert!(kept.contains("`(. ...)` uncurried syntax")); + assert!(!kept.contains("unused variable")); + } + + #[test] + fn retain_critical_external_warnings_handles_crlf_line_endings() { + // Windows stderr from bsc uses CRLF. Without normalization the "\n\n\n" + // splitter would find no boundary and return the entire stream, which + // would effectively disable suppression for external deps on Windows. + let input = concat!( + "\r\n Warning number 26\r\n foo.res:1:1\r\n\r\n unused variable x.\r\n", + "\r\n\r\n\r\n Warning number 3\r\n bar.res:5:10\r\n\r\n ", + "deprecated: The `(. ...)` uncurried syntax is deprecated.\r\n", + ); + let kept = retain_critical_external_warnings(input) + .expect("uncurried-dot warning should survive on Windows too"); + assert!(kept.contains("`(. ...)` uncurried syntax")); + assert!(!kept.contains("unused variable")); + } +} diff --git a/rewatch/src/build/parse.rs b/rewatch/src/build/parse.rs index 223d49d90ea..a33e0b6a1a5 100644 --- a/rewatch/src/build/parse.rs +++ b/rewatch/src/build/parse.rs @@ -1,4 +1,5 @@ use super::build_types::*; +use super::compile::retain_critical_external_warnings; use super::logs; use super::namespaces; use crate::build::packages::Package; @@ -139,7 +140,18 @@ pub fn generate_asts( logs::append(package, &stderr_warnings); stderr.push_str(&stderr_warnings); } - Ok((_path, Some(_))) | Ok((_path, None)) => { + Ok((_path, Some(stderr_warnings))) => { + source_file.implementation.parse_state = ParseState::Success; + source_file.implementation.parse_dirty = false; + // External dep: surface only the critical warnings + // (e.g. legacy `(. ...)` uncurried syntax) so + // downstream users can report breakage upstream. + if let Some(kept) = retain_critical_external_warnings(&stderr_warnings) { + logs::append(package, &kept); + stderr.push_str(&kept); + } + } + Ok((_path, None)) => { source_file.implementation.parse_state = ParseState::Success; source_file.implementation.parse_dirty = false; } @@ -167,7 +179,17 @@ pub fn generate_asts( logs::append(package, &stderr_warnings); stderr.push_str(&stderr_warnings); } - Ok(Some((_, None))) | Ok(Some((_, Some(_)))) => { + Ok(Some((_, Some(stderr_warnings)))) => { + if let Some(interface) = source_file.interface.as_mut() { + interface.parse_state = ParseState::Success; + interface.parse_dirty = false; + } + if let Some(kept) = retain_critical_external_warnings(&stderr_warnings) { + logs::append(package, &kept); + stderr.push_str(&kept); + } + } + Ok(Some((_, None))) => { if let Some(interface) = source_file.interface.as_mut() { interface.parse_state = ParseState::Success; interface.parse_dirty = false; diff --git a/rewatch/tests/compile/18-external-dep-uncurried-dot.sh b/rewatch/tests/compile/18-external-dep-uncurried-dot.sh new file mode 100755 index 00000000000..03d724339dd --- /dev/null +++ b/rewatch/tests/compile/18-external-dep-uncurried-dot.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# Verifies that the legacy `(. args)` uncurried-syntax deprecation warning +# is surfaced even when it originates from an external (node_modules) +# dependency. Unrelated warnings from external deps remain suppressed. + +cd $(dirname $0) +source "../utils.sh" + +bold "Test: Uncurried-dot deprecation warning surfaces from external deps" + +fixture=$(mktemp -d 2>/dev/null || mktemp -d -t rewatch-ext-uncur) +trap "rm -rf '$fixture'" EXIT + +mkdir -p "$fixture/src" +mkdir -p "$fixture/node_modules/legacy-pkg/src" + +cat > "$fixture/package.json" <<'EOF' +{ + "name": "host", + "version": "0.0.1" +} +EOF + +cat > "$fixture/rescript.json" <<'EOF' +{ + "name": "host", + "sources": { "dir": "src" }, + "dependencies": ["legacy-pkg"], + "package-specs": { "module": "commonjs", "in-source": true }, + "suffix": ".bs.js" +} +EOF + +cat > "$fixture/src/Main.res" <<'EOF' +let _ = LegacyPkg.add(1, 2) +EOF + +cat > "$fixture/node_modules/legacy-pkg/package.json" <<'EOF' +{ "name": "legacy-pkg", "version": "0.0.1" } +EOF + +cat > "$fixture/node_modules/legacy-pkg/rescript.json" <<'EOF' +{ + "name": "legacy-pkg", + "sources": { "dir": "src" }, + "package-specs": { "module": "commonjs", "in-source": true }, + "suffix": ".bs.js" +} +EOF + +# Includes: the uncurried-dot deprecation (must surface) and an unused value +# (warning 26, must stay suppressed because it's in an external dep). +cat > "$fixture/node_modules/legacy-pkg/src/LegacyPkg.res" <<'EOF' +let unusedInDep = 99 +let add = (. a, b) => a + b +EOF + +cd "$fixture" +stderr_output=$(rewatch 2>&1 1>/dev/null) +build_status=$? + +if [ $build_status -ne 0 ]; then + error "Build failed" + printf "%s\n" "$stderr_output" >&2 + exit 1 +fi + +if echo "$stderr_output" | grep -qF '`(. ...)` uncurried syntax'; then + success "Uncurried-dot deprecation warning is shown for external dep" +else + error "Expected '(. ...)' uncurried-syntax deprecation in stderr" + printf "%s\n" "$stderr_output" >&2 + exit 1 +fi + +if echo "$stderr_output" | grep -qF 'unused value'; then + error "Unrelated warnings from external deps should be suppressed" + printf "%s\n" "$stderr_output" >&2 + exit 1 +else + success "Unrelated external-dep warnings are still suppressed" +fi diff --git a/rewatch/tests/suite.sh b/rewatch/tests/suite.sh index 613b291d111..b625ce76d45 100755 --- a/rewatch/tests/suite.sh +++ b/rewatch/tests/suite.sh @@ -80,6 +80,7 @@ fi ./compile/12-compile-dev-dependencies.sh && ./compile/13-no-infinite-loop-with-cycle.sh && ./compile/17-prod-flag.sh && +./compile/18-external-dep-uncurried-dot.sh && ./compile/14-no-testrepo-changes.sh && ./compile/15-no-new-files.sh && ./compile/16-snapshots-unchanged.sh && diff --git a/tests/syntax_tests/data/parsing/errors/other/expected/regionMissingComma.res.txt b/tests/syntax_tests/data/parsing/errors/other/expected/regionMissingComma.res.txt index 1a1827c3e76..3202297a2ed 100644 --- a/tests/syntax_tests/data/parsing/errors/other/expected/regionMissingComma.res.txt +++ b/tests/syntax_tests/data/parsing/errors/other/expected/regionMissingComma.res.txt @@ -1,4 +1,15 @@ + Warning number 3 + syntax_tests/data/parsing/errors/other/regionMissingComma.res:2:31 + + 1 │ external make: ( + 2 │ ~style: ReactDOMRe.Style.t=?. + 3 │ ~image: bool=?, + 4 │ ) => React.element = "ModalContent" + + deprecated: The `(. ...)` uncurried syntax is deprecated. Uncurried is now the default in ReScript — remove the leading dot. + + Syntax error! syntax_tests/data/parsing/errors/other/regionMissingComma.res:2:31 diff --git a/tests/syntax_tests/data/parsing/grammar/expressions/expected/uncurriedLegacyDot.res.txt b/tests/syntax_tests/data/parsing/grammar/expressions/expected/uncurriedLegacyDot.res.txt new file mode 100644 index 00000000000..e88dd65280d --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/expressions/expected/uncurriedLegacyDot.res.txt @@ -0,0 +1,77 @@ + + Warning number 3 + syntax_tests/data/parsing/grammar/expressions/uncurriedLegacyDot.res:4:10 + + 2 │ // but emits a deprecation warning. + 3 │ + 4 │ let f = (. x) => x + 1 + 5 │ let g = (. a, b) => a + b + 6 │ let h = (.) => 42 + + deprecated: The `(. ...)` uncurried syntax is deprecated. Uncurried is now the default in ReScript — remove the leading dot. + + + Warning number 3 + syntax_tests/data/parsing/grammar/expressions/uncurriedLegacyDot.res:5:10 + + 3 │ + 4 │ let f = (. x) => x + 1 + 5 │ let g = (. a, b) => a + b + 6 │ let h = (.) => 42 + 7 │ + + deprecated: The `(. ...)` uncurried syntax is deprecated. Uncurried is now the default in ReScript — remove the leading dot. + + + Warning number 3 + syntax_tests/data/parsing/grammar/expressions/uncurriedLegacyDot.res:6:10 + + 4 │ let f = (. x) => x + 1 + 5 │ let g = (. a, b) => a + b + 6 │ let h = (.) => 42 + 7 │ + 8 │ let r1 = f(. 3) + + deprecated: The `(. ...)` uncurried syntax is deprecated. Uncurried is now the default in ReScript — remove the leading dot. + + + Warning number 3 + syntax_tests/data/parsing/grammar/expressions/uncurriedLegacyDot.res:8:12 + + 6 │ let h = (.) => 42 + 7 │ + 8 │ let r1 = f(. 3) + 9 │ let r2 = g(. 1, 2) + 10 │ let r3 = h(.) + + deprecated: The `(. ...)` uncurried syntax is deprecated. Uncurried is now the default in ReScript — remove the leading dot. + + + Warning number 3 + syntax_tests/data/parsing/grammar/expressions/uncurriedLegacyDot.res:9:12 + + 7 │ + 8 │ let r1 = f(. 3) + 9 │ let r2 = g(. 1, 2) + 10 │ let r3 = h(.) + 11 │ + + deprecated: The `(. ...)` uncurried syntax is deprecated. Uncurried is now the default in ReScript — remove the leading dot. + + + Warning number 3 + syntax_tests/data/parsing/grammar/expressions/uncurriedLegacyDot.res:10:12 + + 8 │ let r1 = f(. 3) + 9 │ let r2 = g(. 1, 2) + 10 │ let r3 = h(.) + 11 │ + + deprecated: The `(. ...)` uncurried syntax is deprecated. Uncurried is now the default in ReScript — remove the leading dot. + +let f [arity:1]x = x + 1 +let g [arity:2]a b = a + b +let h [arity:1]() = 42 +let r1 = f 3 +let r2 = g 1 2 +let r3 = h () \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/grammar/expressions/uncurriedLegacyDot.res b/tests/syntax_tests/data/parsing/grammar/expressions/uncurriedLegacyDot.res new file mode 100644 index 00000000000..d234d304294 --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/expressions/uncurriedLegacyDot.res @@ -0,0 +1,10 @@ +// Legacy uncurried `(. ...)` syntax — accepted for backwards compatibility +// but emits a deprecation warning. + +let f = (. x) => x + 1 +let g = (. a, b) => a + b +let h = (.) => 42 + +let r1 = f(. 3) +let r2 = g(. 1, 2) +let r3 = h(.) diff --git a/tests/syntax_tests/data/parsing/grammar/typexpr/expected/uncurriedLegacyDot.res.txt b/tests/syntax_tests/data/parsing/grammar/typexpr/expected/uncurriedLegacyDot.res.txt new file mode 100644 index 00000000000..65a7b3a021d --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/typexpr/expected/uncurriedLegacyDot.res.txt @@ -0,0 +1,38 @@ + + Warning number 3 + syntax_tests/data/parsing/grammar/typexpr/uncurriedLegacyDot.res:4:12 + + 2 │ // compatibility but emits a deprecation warning. + 3 │ + 4 │ type t1 = (. int) => int + 5 │ type t2 = (. int, int) => int + 6 │ type t3 = (. unit) => unit + + deprecated: The `(. ...)` uncurried syntax is deprecated. Uncurried is now the default in ReScript — remove the leading dot. + + + Warning number 3 + syntax_tests/data/parsing/grammar/typexpr/uncurriedLegacyDot.res:5:12 + + 3 │ + 4 │ type t1 = (. int) => int + 5 │ type t2 = (. int, int) => int + 6 │ type t3 = (. unit) => unit + 7 │ + + deprecated: The `(. ...)` uncurried syntax is deprecated. Uncurried is now the default in ReScript — remove the leading dot. + + + Warning number 3 + syntax_tests/data/parsing/grammar/typexpr/uncurriedLegacyDot.res:6:12 + + 4 │ type t1 = (. int) => int + 5 │ type t2 = (. int, int) => int + 6 │ type t3 = (. unit) => unit + 7 │ + + deprecated: The `(. ...)` uncurried syntax is deprecated. Uncurried is now the default in ReScript — remove the leading dot. + +type nonrec t1 = int -> int (a:1) +type nonrec t2 = int -> int -> int (a:2) +type nonrec t3 = unit -> unit (a:1) \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/grammar/typexpr/uncurriedLegacyDot.res b/tests/syntax_tests/data/parsing/grammar/typexpr/uncurriedLegacyDot.res new file mode 100644 index 00000000000..ff0ce7e0621 --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/typexpr/uncurriedLegacyDot.res @@ -0,0 +1,6 @@ +// Legacy uncurried `(. ...)` type syntax — accepted for backwards +// compatibility but emits a deprecation warning. + +type t1 = (. int) => int +type t2 = (. int, int) => int +type t3 = (. unit) => unit