From 6cdc76d46c46f00b9e9e04de221f3dbd399a8990 Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Wed, 22 Apr 2026 09:25:36 +0200 Subject: [PATCH 1/4] Restore legacy `(. ...)` uncurried syntax with deprecation warning Accept the pre-v11 dotted uncurried syntax in function parameters, arguments, and type parameters so projects depending on libraries that still use it can parse again. Emit a `Warnings.Deprecated` on every occurrence, pointing at the leading dot, similar to the deprecation warnings rewatch emits for `bs-*` fields in rescript.json. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 1 + compiler/syntax/src/res_core.ml | 62 ++++++++++++++- compiler/syntax/src/res_grammar.ml | 6 +- .../other/expected/regionMissingComma.res.txt | 11 +++ .../expected/uncurriedLegacyDot.res.txt | 77 +++++++++++++++++++ .../expressions/uncurriedLegacyDot.res | 10 +++ .../expected/uncurriedLegacyDot.res.txt | 38 +++++++++ .../grammar/typexpr/uncurriedLegacyDot.res | 6 ++ 8 files changed, 204 insertions(+), 7 deletions(-) create mode 100644 tests/syntax_tests/data/parsing/grammar/expressions/expected/uncurriedLegacyDot.res.txt create mode 100644 tests/syntax_tests/data/parsing/grammar/expressions/uncurriedLegacyDot.res create mode 100644 tests/syntax_tests/data/parsing/grammar/typexpr/expected/uncurriedLegacyDot.res.txt create mode 100644 tests/syntax_tests/data/parsing/grammar/typexpr/uncurriedLegacyDot.res diff --git a/CHANGELOG.md b/CHANGELOG.md index 314e08424e1..d2485108cd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,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. #### :house: Internal diff --git a/compiler/syntax/src/res_core.ml b/compiler/syntax/src/res_core.ml index 8583dab8fb7..4c115a622b2 100644 --- a/compiler/syntax/src/res_core.ml +++ b/compiler/syntax/src/res_core.ml @@ -286,6 +286,19 @@ let tagged_template_literal_attr = let spread_attr = (Location.mknoloc "res.spread", 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 = { @@ -1858,6 +1871,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 @@ -1875,10 +1891,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 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; @@ -1978,6 +1998,7 @@ and parse_parameter_list p = * | _ * | lident * | () + * | (.) (* deprecated uncurried syntax *) * | ( parameter {, parameter} [,] ) *) and parse_parameters p : fundef_type_param option * fundef_term_param list = @@ -2026,6 +2047,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 ()] @@ -3964,13 +3989,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 = @@ -4668,6 +4711,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 = @@ -4678,8 +4724,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 -> ( diff --git a/compiler/syntax/src/res_grammar.ml b/compiler/syntax/src/res_grammar.ml index c33e6dcebdb..f287f875671 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/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 From adb98919c10b067d915c03950f9233c4de9b8e71 Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Wed, 22 Apr 2026 09:39:54 +0200 Subject: [PATCH 2/4] Rewatch: surface uncurried-dot deprecation for external deps Rewatch suppresses warnings from external dependencies (users can't act on them), but the legacy `(. ...)` uncurried-syntax deprecation is a signal consumers need to see so they can report breakage upstream before the syntax is removed. Add a small allow-list in the stderr capture path for both the AST-parse phase and the compile phase that keeps warning blocks mentioning that specific deprecation while still dropping everything else from external packages. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 2 +- rewatch/src/build/compile.rs | 57 ++++++++++++- rewatch/src/build/parse.rs | 26 +++++- .../compile/18-external-dep-uncurried-dot.sh | 82 +++++++++++++++++++ rewatch/tests/suite.sh | 1 + 5 files changed, 163 insertions(+), 5 deletions(-) create mode 100755 rewatch/tests/compile/18-external-dep-uncurried-dot.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index d2485108cd0..f7bad5cad96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,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. +- 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. #### :house: Internal diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index 5247342a2a3..3b7bfcbd530 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -940,10 +940,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) @@ -952,6 +955,33 @@ 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). Split on that boundary, keep the blocks + // that mention the marker, and re-join with the same separator so the + // output is indistinguishable from the original. + let kept: Vec<&str> = stderr + .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) { @@ -1048,3 +1078,26 @@ 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")); + } +} 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 b7f6bee6146..2ee0e7b876c 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 && From 129bf580eadc5701a09b0a2167d39c7112061c1e Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Wed, 22 Apr 2026 14:36:36 +0200 Subject: [PATCH 3/4] Fix ocamlformat of res_core.ml and add PR link to changelog Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 2 +- compiler/syntax/src/res_core.ml | 36 ++++++++++++++++----------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7bad5cad96..77f979ce4c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,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. +- 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 #### :house: Internal diff --git a/compiler/syntax/src/res_core.ml b/compiler/syntax/src/res_core.ml index 4c115a622b2..944ddbe8801 100644 --- a/compiler/syntax/src/res_core.ml +++ b/compiler/syntax/src/res_core.ml @@ -1893,12 +1893,12 @@ and parse_parameter p = if 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); + 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; @@ -1983,7 +1983,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 = @@ -2047,10 +2047,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); + 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 ()] @@ -3999,11 +3999,11 @@ and parse_argument p : argument option = || Grammar.is_expr_start p.token then match p.Parser.token with - | Dot -> + | 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 + match p.token with (* apply(.) — legacy uncurried unit call *) | Rparen -> let unit_expr = @@ -4728,12 +4728,12 @@ and parse_type_parameter ?current_type_name_path ?inline_types_context p.Parser.token = Token.Tilde || p.token = Dot || Grammar.is_typ_expr_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); + 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 -> ( @@ -4805,7 +4805,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) *) From df04831570f1ef25497e098ee342918125853904 Mon Sep 17 00:00:00 2001 From: Jaap Frolich Date: Wed, 22 Apr 2026 15:06:40 +0200 Subject: [PATCH 4/4] Rewatch: normalize CRLF before splitting warning blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "\n\n\n" block separator used by retain_critical_external_warnings assumes LF line endings. On Windows bsc emits CRLF, so the splitter would find no boundary and return the entire stderr — effectively disabling external-dep warning suppression for any package that emits the uncurried-dot deprecation alongside other warnings. Normalize CRLF → LF before splitting. Add a CRLF test that exercises the Windows-shaped input. Co-Authored-By: Claude Opus 4.7 (1M context) --- rewatch/src/build/compile.rs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index 3b7bfcbd530..73993a039c3 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -968,10 +968,12 @@ pub(super) fn retain_critical_external_warnings(stderr: &str) -> Option return None; } // bsc prints each warning as its own block separated by a blank-line pair - // (three consecutive newlines). Split on that boundary, keep the blocks - // that mention the marker, and re-join with the same separator so the - // output is indistinguishable from the original. - let kept: Vec<&str> = stderr + // (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(); @@ -1100,4 +1102,20 @@ mod tests { 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")); + } }