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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 61 additions & 7 deletions compiler/syntax/src/res_core.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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 =
Expand All @@ -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 =
Expand Down Expand Up @@ -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 ()]
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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 =
Expand All @@ -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 -> (
Expand Down Expand Up @@ -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) *)
Expand Down
6 changes: 3 additions & 3 deletions compiler/syntax/src/res_grammar.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
75 changes: 73 additions & 2 deletions rewatch/src/build/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<String> {
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))
Comment on lines +1132 to +1133
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize line endings before splitting warning blocks

The filter assumes warning blocks are separated by "\n\n\n", but on Windows stderr commonly uses CRLF ("\r\n"), so this split does not break the stream into blocks. When an external dependency emits both the uncurried-dot deprecation and unrelated warnings, retain_critical_external_warnings will keep the entire stderr payload instead of only the critical block, effectively disabling suppression for those packages in Windows builds (this helper is used by both parse and compile warning paths).

Useful? React with 👍 / 👎.

.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) {
Expand Down Expand Up @@ -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"));
}
}
26 changes: 24 additions & 2 deletions rewatch/src/build/parse.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading