Skip to content

Fix issues with overlapping argument name and default value, using alias and default value together #5989

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Feb 8, 2023
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ These are only breaking changes for unformatted code.
- Fix issue with nested async functions, where the inner function would be emitted without `async` https://github.com/rescript-lang/rescript-compiler/pull/5983
- Fix issue with async context check, and printer, for async functions with locally abstract type https://github.com/rescript-lang/rescript-compiler/pull/5982
- Fix support for recursive components in JSX V4 https://github.com/rescript-lang/rescript-compiler/pull/5986
- Fix issue with overlapping labelled argument with default value https://github.com/rescript-lang/rescript-compiler/pull/5989
- Fix issue with using alias and default value together https://github.com/rescript-lang/rescript-compiler/pull/5989

#### :nail_care: Polish

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions jscomp/test/alias_default_value_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use strict';


function Alias_default_value_test$C0(props) {
var a = props.a;
var a$1 = a !== undefined ? a : 2;
var b = props.b;
var b$1 = b !== undefined ? b : (a$1 << 1);
return a$1 + b$1 | 0;
}

var C0 = {
make: Alias_default_value_test$C0
};

function Alias_default_value_test$C1(props) {
var foo = props.foo;
if (foo !== undefined) {
return foo;
} else {
return "";
}
}

var C1 = {
make: Alias_default_value_test$C1
};

exports.C0 = C0;
exports.C1 = C1;
/* No side effect */
17 changes: 17 additions & 0 deletions jscomp/test/alias_default_value_test.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@@bs.config({
flags: ["-bs-jsx", "4"],
})

module C0 = {
@react.component
let make = (~a=2, ~b=a * 2) => {
React.int(a + b)
}
}

module C1 = {
@react.component
let make = (~foo as bar="") => {
React.string(bar)
}
}
3 changes: 2 additions & 1 deletion jscomp/test/build.ninja

Large diffs are not rendered by default.

72 changes: 48 additions & 24 deletions res_syntax/src/reactjs_jsx_v4.ml
Original file line number Diff line number Diff line change
Expand Up @@ -729,10 +729,10 @@ let argToType ~newtypes ~(typeConstraints : core_type option) types
:: types
| _ -> types

let argWithDefaultValue (name, default, _, _, _, _) =
match default with
| Some default when isOptional name -> Some (getLabel name, default)
| _ -> None
let hasDefaultValue nameArgList =
nameArgList
|> List.exists (fun (name, default, _, _, _, _) ->
Option.is_some default && isOptional name)

let argToConcreteType types (name, attrs, loc, type_) =
match name with
Expand Down Expand Up @@ -1037,26 +1037,43 @@ let transformStructureItem ~config mapper item =
(argToType ~newtypes ~typeConstraints)
[] namedArgList
in
let namedArgWithDefaultValueList =
List.filter_map argWithDefaultValue namedArgList
let vbMatch (name, default, _, alias, loc, _) =
let label = getLabel name in
match default with
| Some default ->
Vb.mk
(Pat.var (Location.mkloc alias loc))
(Exp.match_
(Exp.field
(Exp.ident {txt = Lident "props"; loc = Location.none})
(Location.mknoloc @@ Lident label))
[
Exp.case
(Pat.construct
(Location.mknoloc @@ Lident "Some")
(Some (Pat.var (Location.mknoloc label))))
(Exp.ident (Location.mknoloc @@ Lident label));
Exp.case
(Pat.construct (Location.mknoloc @@ Lident "None") None)
default;
])
| None ->
Vb.mk
(Pat.var (Location.mkloc alias loc))
(Exp.field
(Exp.ident {txt = Lident "props"; loc = Location.none})
(Location.mknoloc @@ Lident label))
in
let vbMatch (label, default) =
Vb.mk
(Pat.var (Location.mknoloc label))
(Exp.match_
(Exp.ident {txt = Lident label; loc = Location.none})
[
Exp.case
(Pat.construct
(Location.mknoloc @@ Lident "Some")
(Some (Pat.var (Location.mknoloc label))))
(Exp.ident (Location.mknoloc @@ Lident label));
Exp.case
(Pat.construct (Location.mknoloc @@ Lident "None") None)
default;
])
let vbMatchExpr namedArgList expr =
let rec aux namedArgList =
match namedArgList with
| [] -> expr
| [namedArg] -> Exp.let_ Nonrecursive [vbMatch namedArg] expr
| namedArg :: rest ->
Exp.let_ Nonrecursive [vbMatch namedArg] (aux rest)
in
aux (List.rev namedArgList)
in
let vbMatchList = List.map vbMatch namedArgWithDefaultValueList in
(* type props = { ... } *)
let propsRecordType =
makePropsRecordType ~coreTypeOfAttr ~typVarsOfCoreType "props"
Expand Down Expand Up @@ -1175,20 +1192,27 @@ let transformStructureItem ~config mapper item =
in
(* add pattern matching for optional prop value *)
let expression =
if List.length vbMatchList = 0 then expression
else Exp.let_ Nonrecursive vbMatchList expression
if hasDefaultValue namedArgList then
vbMatchExpr namedArgList expression
else expression
in
(* (ref) => expr *)
let expression =
List.fold_left
(fun expr (_, pattern) -> Exp.fun_ Nolabel None pattern expr)
expression patternsWithNolabel
in
(* ({a, b, _}: props<'a, 'b>) *)
let recordPattern =
match patternsWithLabel with
| [] -> Pat.any ()
| _ -> Pat.record (List.rev patternsWithLabel) Open
in
let recordPattern =
if hasDefaultValue namedArgList then
Pat.var {txt = "props"; loc = emptyLoc}
else recordPattern
in
let expression =
Exp.fun_ Nolabel None
(Pat.constraint_ recordPattern
Expand Down
5 changes: 5 additions & 0 deletions res_syntax/tests/ppx/react/aliasProps.res
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ module C1 = {
@react.component
let make = (~priority as p, ~text="Test") => React.string(p ++ text)
}

module C2 = {
@react.component
let make = (~foo as bar="") => React.string(bar)
}
9 changes: 9 additions & 0 deletions res_syntax/tests/ppx/react/defaultValueProp.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module C0 = {
@react.component
let make = (~a=2, ~b=a*2) => React.int(a + b)
}

module C1 = {
@react.component
let make = (~a=2, ~b) => React.int(a + b)
}
29 changes: 25 additions & 4 deletions res_syntax/tests/ppx/react/expected/aliasProps.res.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ module C0 = {
type props<'priority, 'text> = {priority: 'priority, text?: 'text}

@react.component
let make = ({priority: _, ?text, _}: props<'priority, 'text>) => {
let text = switch text {
let make = (props: props<'priority, 'text>) => {
let _ = props.priority
let text = switch props.text {
| Some(text) => text
| None => "Test"
}
Expand All @@ -23,8 +24,9 @@ module C1 = {
type props<'priority, 'text> = {priority: 'priority, text?: 'text}

@react.component
let make = ({priority: p, ?text, _}: props<'priority, 'text>) => {
let text = switch text {
let make = (props: props<'priority, 'text>) => {
let p = props.priority
let text = switch props.text {
| Some(text) => text
| None => "Test"
}
Expand All @@ -37,3 +39,22 @@ module C1 = {
\"AliasProps$C1"
}
}

module C2 = {
type props<'foo> = {foo?: 'foo}

@react.component
let make = (props: props<'foo>) => {
let bar = switch props.foo {
| Some(foo) => foo
| None => ""
}

React.string(bar)
}
let make = {
let \"AliasProps$C2" = (props: props<_>) => make(props)

\"AliasProps$C2"
}
}
40 changes: 40 additions & 0 deletions res_syntax/tests/ppx/react/expected/defaultValueProp.res.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module C0 = {
type props<'a, 'b> = {a?: 'a, b?: 'b}
@react.component
let make = (props: props<'a, 'b>) => {
let a = switch props.a {
| Some(a) => a
| None => 2
}
let b = switch props.b {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Given the error message I'm getting "The module or file props can't be found." this is probably a path A.foo where A happens to be props and not a field access, which it should be.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Field access is Pexp_field.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oops! Correct.

| Some(b) => b
| None => a * 2
Copy link
Collaborator

@cristianoc cristianoc Feb 7, 2023

Choose a reason for hiding this comment

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

If you copy paste this, you'll get a a not found.
The oder should be: first let a then b.
Also, it should not be let ... and but let ... let or it won't compile.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, a should refer to the outside scope of make function. The original code is this:

@react.component
let make = (~a=2, ~b=a*2) => <div />

So, a in (a* 2) is referring to out of this make function.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is quite tricky than I guess. It has to be affected by the order, but it should not.

let make = (props) => {
  let a = 2 // 1
  let b = a * 2 // <- a should be reference to the outside of make function, but it refers to 1

Copy link
Member Author

Choose a reason for hiding this comment

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

We transform the optional labelled argument to nolabel argument props. So there no where to handle the default value that is the root cause of this issue, I guess.

}

React.int(a + b)
}
let make = {
let \"DefaultValueProp$C0" = (props: props<_>) => make(props)
\"DefaultValueProp$C0"
}
}

module C1 = {
type props<'a, 'b> = {a?: 'a, b: 'b}

@react.component
let make = (props: props<'a, 'b>) => {
let a = switch props.a {
| Some(a) => a
| None => 2
}
let b = props.b

React.int(a + b)
}
let make = {
let \"DefaultValueProp$C1" = (props: props<_>) => make(props)

\"DefaultValueProp$C1"
}
}
8 changes: 4 additions & 4 deletions res_syntax/tests/ppx/react/expected/uncurriedProps.res.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
type props<'a> = {a?: 'a}

@react.component
let make = ({?a, _}: props<(. unit) => unit>) => {
let a = switch a {
let make = (props: props<(. unit) => unit>) => {
let a = switch props.a {
| Some(a) => a
| None => (. ()) => ()
}
Expand All @@ -30,8 +30,8 @@ module Foo = {
type props<'callback> = {callback?: 'callback}

@react.component
let make = ({?callback, _}: props<(. string, bool, bool) => unit>) => {
let callback = switch callback {
let make = (props: props<(. string, bool, bool) => unit>) => {
let callback = switch props.callback {
| Some(callback) => callback
| None => (. _, _, _) => ()
}
Expand Down