From 8b1471dc086828d0c7fe10eac2afc117b23e8ec9 Mon Sep 17 00:00:00 2001 From: Iwan Date: Sat, 27 Feb 2021 11:22:59 +0100 Subject: [PATCH 1/8] Add support for object type spreading ```rescript type a = {"x": int} type u = {...a, "y": int} ``` --- src/res_core.ml | 25 +++++++++++++++++++ src/res_grammar.ml | 2 +- src/res_printer.ml | 6 ++++- .../typexpr/__snapshots__/parse.spec.js.snap | 7 ++++++ .../grammar/typexpr/objectTypeSpreading.res | 4 +++ .../typexpr/__snapshots__/render.spec.js.snap | 8 ++++++ tests/printer/typexpr/objectTypeSpreading.res | 4 +++ 7 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 tests/parsing/grammar/typexpr/objectTypeSpreading.res create mode 100644 tests/printer/typexpr/objectTypeSpreading.res diff --git a/src/res_core.ml b/src/res_core.ml index a98aa451..57625108 100644 --- a/src/res_core.ml +++ b/src/res_core.ml @@ -4130,6 +4130,10 @@ and parseStringFieldDeclaration p = Parser.expect ~grammar:Grammar.TypeExpression Colon p; let typ = parsePolyTypeExpr p in Some(Parsetree.Otag (fieldName, attrs, typ)) + | DotDotDot -> + Parser.next p; + let typ = parseTypExpr p in + Some(Parsetree.Oinherit typ) | Lident name -> let nameLoc = mkLoc p.startPos p.endPos in Parser.err p (Diagnostics.message "An inline record type declaration is only allowed in a variant constructor's declaration"); @@ -4633,6 +4637,27 @@ and parseRecordOrBsObjectDecl p = in let typ = parseArrowTypeRest ~es6Arrow:true ~startPos typ p in (Some typ, Asttypes.Public, Parsetree.Ptype_abstract) + | DotDotDot -> + (* start of object type spreading, e.g. `type u = {...a, "u": int}` *) + Parser.next p; + let typ = parseTypExpr p in + Parser.expect Comma p; + let fields = + (Parsetree.Oinherit typ)::( + parseCommaDelimitedRegion + ~grammar:Grammar.StringFieldDeclarations + ~closing:Rbrace + ~f:parseStringFieldDeclaration + p + ) + in + Parser.expect Rbrace p; + let loc = mkLoc startPos p.prevEndPos in + let typ = + Ast_helper.Typ.object_ ~loc fields Asttypes.Closed |> parseTypeAlias p + in + let typ = parseArrowTypeRest ~es6Arrow:true ~startPos typ p in + (Some typ, Asttypes.Public, Parsetree.Ptype_abstract) | _ -> let attrs = parseAttributes p in begin match p.Parser.token with diff --git a/src/res_grammar.ml b/src/res_grammar.ml index e2b2b317..99e74201 100644 --- a/src/res_grammar.ml +++ b/src/res_grammar.ml @@ -211,7 +211,7 @@ let isParameterStart = function (* TODO: overparse Uident ? *) let isStringFieldDeclStart = function - | Token.String _ | Lident _ | At -> true + | Token.String _ | Lident _ | At | DotDotDot -> true | _ -> false (* TODO: overparse Uident ? *) diff --git a/src/res_printer.ml b/src/res_printer.ml index ab6dc43a..a3547470 100644 --- a/src/res_printer.ml +++ b/src/res_printer.ml @@ -1761,7 +1761,11 @@ and printObjectField (field : Parsetree.object_field) cmtTbl = ] in let cmtLoc = {labelLoc.loc with loc_end = typ.ptyp_loc.loc_end} in printComments doc cmtTbl cmtLoc - | _ -> Doc.nil + | Oinherit typexpr -> + Doc.concat [ + Doc.dotdotdot; + printTypExpr typexpr cmtTbl + ] (* es6 arrow type arg * type t = (~foo: string, ~bar: float=?, unit) => unit diff --git a/tests/parsing/grammar/typexpr/__snapshots__/parse.spec.js.snap b/tests/parsing/grammar/typexpr/__snapshots__/parse.spec.js.snap index 59586f00..66f254ac 100644 --- a/tests/parsing/grammar/typexpr/__snapshots__/parse.spec.js.snap +++ b/tests/parsing/grammar/typexpr/__snapshots__/parse.spec.js.snap @@ -119,6 +119,13 @@ type nonrec t = (module Console) ref let (devices : (string, (module DEVICE)) Hastbl.t) = Hashtbl.creat 17" `; +exports[`objectTypeSpreading.res 1`] = ` +"type nonrec a = < x: int > +type nonrec u = < a ;u: int > +type nonrec v = < v: int ;a > +type nonrec w = < j: int ;a ;k: int ;v > " +`; + exports[`parenthesized.res 1`] = `"type nonrec t = ((a:((int)[@ns.namedArgLoc ]) -> unit)[@attr ])"`; exports[`poly.res 1`] = ` diff --git a/tests/parsing/grammar/typexpr/objectTypeSpreading.res b/tests/parsing/grammar/typexpr/objectTypeSpreading.res new file mode 100644 index 00000000..03343b15 --- /dev/null +++ b/tests/parsing/grammar/typexpr/objectTypeSpreading.res @@ -0,0 +1,4 @@ +type a = {"x": int} +type u = {...a, "u": int} +type v = {"v": int, ...a} +type w = {"j": int, ...a, "k": int, ...v} diff --git a/tests/printer/typexpr/__snapshots__/render.spec.js.snap b/tests/printer/typexpr/__snapshots__/render.spec.js.snap index a1a07b28..2efd1265 100644 --- a/tests/printer/typexpr/__snapshots__/render.spec.js.snap +++ b/tests/printer/typexpr/__snapshots__/render.spec.js.snap @@ -409,6 +409,14 @@ let devices: @attr Hastbl.t = xyz " `; +exports[`objectTypeSpreading.res 1`] = ` +"type a = {\\"x\\": int} +type u = {...a, \\"u\\": int} +type v = {\\"v\\": int, ...a} +type w = {\\"j\\": int, ...a, \\"k\\": int, ...v} +" +`; + exports[`polyTyp.res 1`] = ` "external getLogger: unit => { \\"log\\": 'a => unit, diff --git a/tests/printer/typexpr/objectTypeSpreading.res b/tests/printer/typexpr/objectTypeSpreading.res new file mode 100644 index 00000000..03343b15 --- /dev/null +++ b/tests/printer/typexpr/objectTypeSpreading.res @@ -0,0 +1,4 @@ +type a = {"x": int} +type u = {...a, "u": int} +type v = {"v": int, ...a} +type w = {"j": int, ...a, "k": int, ...v} From 69fd8ccc87add5ea1fa39b4ca8a63e0b2e083628 Mon Sep 17 00:00:00 2001 From: Iwan Date: Sun, 28 Feb 2021 08:08:53 +0100 Subject: [PATCH 2/8] Improve test coverage for object type spread. --- src/res_core.ml | 29 +++++++++++++ .../typexpr/__snapshots__/parse.spec.js.snap | 37 +++++++++++++++- .../grammar/typexpr/objectTypeSpreading.res | 36 ++++++++++++++++ .../typexpr/__snapshots__/render.spec.js.snap | 42 +++++++++++++++++++ tests/printer/typexpr/objectTypeSpreading.res | 36 ++++++++++++++++ 5 files changed, 179 insertions(+), 1 deletion(-) diff --git a/src/res_core.ml b/src/res_core.ml index 57625108..0f31e170 100644 --- a/src/res_core.ml +++ b/src/res_core.ml @@ -4260,6 +4260,35 @@ and parseConstrDeclArgs p = in Parser.expect Rparen p; Parsetree.Pcstr_tuple (typ::moreArgs) + | DotDotDot -> + (* start of object type spreading, e.g. `User({...a, "u": int})` *) + Parser.next p; + let typ = parseTypExpr p in + Parser.expect Comma p; + let fields = + (Parsetree.Oinherit typ)::( + parseCommaDelimitedRegion + ~grammar:Grammar.StringFieldDeclarations + ~closing:Rbrace + ~f:parseStringFieldDeclaration + p + ) + in + Parser.expect Rbrace p; + let loc = mkLoc startPos p.prevEndPos in + let typ = + Ast_helper.Typ.object_ ~loc fields Asttypes.Closed |> parseTypeAlias p + in + let typ = parseArrowTypeRest ~es6Arrow:true ~startPos typ p in + Parser.optional p Comma |> ignore; + let moreArgs = + parseCommaDelimitedRegion + ~grammar:Grammar.TypExprList + ~closing:Rparen + ~f:parseTypExprRegion p + in + Parser.expect Rparen p; + Parsetree.Pcstr_tuple (typ::moreArgs) | _ -> let attrs = parseAttributes p in begin match p.Parser.token with diff --git a/tests/parsing/grammar/typexpr/__snapshots__/parse.spec.js.snap b/tests/parsing/grammar/typexpr/__snapshots__/parse.spec.js.snap index 66f254ac..d59ba55c 100644 --- a/tests/parsing/grammar/typexpr/__snapshots__/parse.spec.js.snap +++ b/tests/parsing/grammar/typexpr/__snapshots__/parse.spec.js.snap @@ -123,7 +123,42 @@ exports[`objectTypeSpreading.res 1`] = ` "type nonrec a = < x: int > type nonrec u = < a ;u: int > type nonrec v = < v: int ;a > -type nonrec w = < j: int ;a ;k: int ;v > " +type nonrec w = < j: int ;a ;k: int ;v > +type nonrec t = < a ;u: int > as 'a +type nonrec t = < a ;u: int > -> unit +type nonrec t = (< a ;u: int > as 'a) -> unit +type nonrec t = < a ;u: int > -> < a ;v: int > -> unit +type nonrec user = < name: string > +let (steve : < user ;age: int > ) = [%obj { name = \\"Steve\\"; age = 30 }] +let steve = ([%obj { name = \\"Steve\\"; age = 30 }] : < user ;age: int > ) +let steve = ((([%obj { name = \\"Steve\\"; age = 30 }] : < user ;age: int > )) + [@ns.braces ]) +let printFullUser (steve : < user ;age: int > ) = Js.log steve +let printFullUser ~user:(((user : < user ;age: int > ))[@ns.namedArgLoc ]) + = Js.log steve +let printFullUser ~user:(((user : < user ;age: int > ))[@ns.namedArgLoc ]) + = Js.log steve +let printFullUser ?user:(((user)[@ns.namedArgLoc ])= + (steve : < user ;age: int > )) = Js.log steve +external steve : < user ;age: int > = \\"steve\\"[@@val ] +let makeCeoOf30yearsOld name = + ([%obj { name; age = 30 }] : < user ;age: int > ) +type nonrec optionalUser = < user ;age: int > option +type nonrec optionalTupleUser = + (< user ;age: int > * < user ;age: int > ) option +type nonrec constrUser = + (< user ;age: int > , < user ;age: int > ) myTypeConstructor +type nonrec taggedUser = + | User of < user ;age: int > + | Ceo of < user ;age: int ;direction: bool > * + < salary ;taxFraud: bool > +type nonrec polyTaggedUser = [ \`User of < user ;age: int > ] +type nonrec polyTaggedUser2 = + [ \`User of < user ;age: int > + | \`Ceo of + (< user ;age: int ;direction: bool > * + < salary ;taxFraud: bool > ) + ]" `; exports[`parenthesized.res 1`] = `"type nonrec t = ((a:((int)[@ns.namedArgLoc ]) -> unit)[@attr ])"`; diff --git a/tests/parsing/grammar/typexpr/objectTypeSpreading.res b/tests/parsing/grammar/typexpr/objectTypeSpreading.res index 03343b15..216d4000 100644 --- a/tests/parsing/grammar/typexpr/objectTypeSpreading.res +++ b/tests/parsing/grammar/typexpr/objectTypeSpreading.res @@ -2,3 +2,39 @@ type a = {"x": int} type u = {...a, "u": int} type v = {"v": int, ...a} type w = {"j": int, ...a, "k": int, ...v} + + +type t = {...a, "u": int} as 'a +type t = {...a, "u": int} => unit +type t = {...a, "u": int} as 'a => unit +type t = ({...a, "u": int}, {...a, "v": int}) => unit + +type user = {"name": string} + +let steve: {...user, "age": int} = {"name": "Steve", "age": 30} +let steve = ({"name": "Steve", "age": 30}: {...user, "age": int}) +let steve = {({"name": "Steve", "age": 30}: {...user, "age": int})} + +let printFullUser = (steve: {...user, "age": int}) => Js.log(steve) +let printFullUser = (~user: {...user, "age": int}) => Js.log(steve) +let printFullUser = (~user: {...user, "age": int}) => Js.log(steve) +let printFullUser = (~user = steve : {...user, "age": int}) => Js.log(steve) + +@val +external steve: {...user, "age": int} = "steve" + +let makeCeoOf30yearsOld = (name) : {...user, "age": int} => {"name": name, "age": 30} + +type optionalUser = option<{...user, "age": int}> +type optionalTupleUser = option<({...user, "age": int}, {...user, "age": int})> +type constrUser = myTypeConstructor<{...user, "age": int}, {...user, "age": int}> + +type taggedUser = + | User({...user, "age": int}) + | Ceo({...user, "age": int, "direction": bool}, {...salary, "taxFraud": bool}) + +type polyTaggedUser = [ #User({...user, "age": int}) ] +type polyTaggedUser2 = [ + | #User({...user, "age": int}) + | #Ceo({...user, "age": int, "direction": bool}, {...salary, "taxFraud": bool}) +] diff --git a/tests/printer/typexpr/__snapshots__/render.spec.js.snap b/tests/printer/typexpr/__snapshots__/render.spec.js.snap index 2efd1265..b53c5cc6 100644 --- a/tests/printer/typexpr/__snapshots__/render.spec.js.snap +++ b/tests/printer/typexpr/__snapshots__/render.spec.js.snap @@ -414,6 +414,48 @@ exports[`objectTypeSpreading.res 1`] = ` type u = {...a, \\"u\\": int} type v = {\\"v\\": int, ...a} type w = {\\"j\\": int, ...a, \\"k\\": int, ...v} + +type t = {...a, \\"u\\": int} as 'a +type t = {...a, \\"u\\": int} => unit +type t = ({...a, \\"u\\": int} as 'a) => unit +type t = ({...a, \\"u\\": int}, {...a, \\"v\\": int}) => unit + +type user = {\\"name\\": string} + +let steve: {...user, \\"age\\": int} = {\\"name\\": \\"Steve\\", \\"age\\": 30} +let steve = ({\\"name\\": \\"Steve\\", \\"age\\": 30}: {...user, \\"age\\": int}) +let steve = {({\\"name\\": \\"Steve\\", \\"age\\": 30}: {...user, \\"age\\": int})} + +let printFullUser = (steve: {...user, \\"age\\": int}) => Js.log(steve) +let printFullUser = (~user: {...user, \\"age\\": int}) => Js.log(steve) +let printFullUser = (~user: {...user, \\"age\\": int}) => Js.log(steve) +let printFullUser = (~user=steve: {...user, \\"age\\": int}) => Js.log(steve) + +@val +external steve: {...user, \\"age\\": int} = \\"steve\\" + +let makeCeoOf30yearsOld = (name): {...user, \\"age\\": int} => + {\\"name\\": name, \\"age\\": 30} + +type optionalUser = option<{...user, \\"age\\": int}> +type optionalTupleUser = option<({...user, \\"age\\": int}, {...user, \\"age\\": int})> +type constrUser = myTypeConstructor< + {...user, \\"age\\": int}, + {...user, \\"age\\": int}, +> + +type taggedUser = + | User({...user, \\"age\\": int}) + | Ceo({...user, \\"age\\": int, \\"direction\\": bool}, {...salary, \\"taxFraud\\": bool}) + +type polyTaggedUser = [#User({...user, \\"age\\": int})] +type polyTaggedUser2 = [ + | #User({...user, \\"age\\": int}) + | #Ceo( + {...user, \\"age\\": int, \\"direction\\": bool}, + {...salary, \\"taxFraud\\": bool}, + ) +] " `; diff --git a/tests/printer/typexpr/objectTypeSpreading.res b/tests/printer/typexpr/objectTypeSpreading.res index 03343b15..216d4000 100644 --- a/tests/printer/typexpr/objectTypeSpreading.res +++ b/tests/printer/typexpr/objectTypeSpreading.res @@ -2,3 +2,39 @@ type a = {"x": int} type u = {...a, "u": int} type v = {"v": int, ...a} type w = {"j": int, ...a, "k": int, ...v} + + +type t = {...a, "u": int} as 'a +type t = {...a, "u": int} => unit +type t = {...a, "u": int} as 'a => unit +type t = ({...a, "u": int}, {...a, "v": int}) => unit + +type user = {"name": string} + +let steve: {...user, "age": int} = {"name": "Steve", "age": 30} +let steve = ({"name": "Steve", "age": 30}: {...user, "age": int}) +let steve = {({"name": "Steve", "age": 30}: {...user, "age": int})} + +let printFullUser = (steve: {...user, "age": int}) => Js.log(steve) +let printFullUser = (~user: {...user, "age": int}) => Js.log(steve) +let printFullUser = (~user: {...user, "age": int}) => Js.log(steve) +let printFullUser = (~user = steve : {...user, "age": int}) => Js.log(steve) + +@val +external steve: {...user, "age": int} = "steve" + +let makeCeoOf30yearsOld = (name) : {...user, "age": int} => {"name": name, "age": 30} + +type optionalUser = option<{...user, "age": int}> +type optionalTupleUser = option<({...user, "age": int}, {...user, "age": int})> +type constrUser = myTypeConstructor<{...user, "age": int}, {...user, "age": int}> + +type taggedUser = + | User({...user, "age": int}) + | Ceo({...user, "age": int, "direction": bool}, {...salary, "taxFraud": bool}) + +type polyTaggedUser = [ #User({...user, "age": int}) ] +type polyTaggedUser2 = [ + | #User({...user, "age": int}) + | #Ceo({...user, "age": int, "direction": bool}, {...salary, "taxFraud": bool}) +] From be83c83c0d9fa85fbd26cff53faa58eec278bcc9 Mon Sep 17 00:00:00 2001 From: Iwan Date: Sun, 28 Feb 2021 11:03:08 +0100 Subject: [PATCH 3/8] Cleanup function names containing "BsObject" --- src/res_core.ml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/res_core.ml b/src/res_core.ml index 0f31e170..412b1e47 100644 --- a/src/res_core.ml +++ b/src/res_core.ml @@ -3766,7 +3766,7 @@ and parseAtomicTypExpr ~attrs p = let loc = mkLoc startPos p.prevEndPos in Ast_helper.Typ.extension ~attrs ~loc extension | Lbrace -> - parseRecordOrBsObjectType ~attrs p + parseRecordOrObjectType ~attrs p | token -> Parser.err p (Diagnostics.unexpected token p.breadcrumbs); begin match skipTokensAndMaybeRetry p ~isStartOfGrammar:Grammar.isAtomicTypExprStart with @@ -3825,7 +3825,7 @@ and parsePackageConstraint p = Some (typeConstr, typ) | _ -> None -and parseRecordOrBsObjectType ~attrs p = +and parseRecordOrObjectType ~attrs p = (* for inline record in constructor *) let startPos = p.Parser.startPos in Parser.expect Lbrace p; @@ -4641,7 +4641,7 @@ and parseTypeEquationOrConstrDecl p = (* TODO: is this a good idea? *) (None, Asttypes.Public, Parsetree.Ptype_abstract) -and parseRecordOrBsObjectDecl p = +and parseRecordOrObjectDecl p = let startPos = p.Parser.startPos in Parser.expect Lbrace p; match p.Parser.token with @@ -4777,7 +4777,7 @@ and parsePrivateEqOrRepr p = Parser.expect Private p; match p.Parser.token with | Lbrace -> - let (manifest, _ ,kind) = parseRecordOrBsObjectDecl p in + let (manifest, _ ,kind) = parseRecordOrObjectDecl p in (manifest, Asttypes.Private, kind) | Uident _ -> let (manifest, _, kind) = parseTypeEquationOrConstrDecl p in @@ -4979,7 +4979,7 @@ and parseTypeEquationAndRepresentation p = | Uident _ -> parseTypeEquationOrConstrDecl p | Lbrace -> - parseRecordOrBsObjectDecl p + parseRecordOrObjectDecl p | Private -> parsePrivateEqOrRepr p | Bar | DotDot -> From b1f34093d9b5aa45945111d00a92c62d3f9e2b62 Mon Sep 17 00:00:00 2001 From: Iwan Date: Sun, 28 Feb 2021 11:41:00 +0100 Subject: [PATCH 4/8] Improve error messages when user tries to mix object/record declaration with spreading. --- src/res_core.ml | 32 ++++++++- .../typexpr/__snapshots__/parse.spec.js.snap | 65 +++++++++++++++++++ tests/parsing/errors/typexpr/objectSpread.res | 9 +++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 tests/parsing/errors/typexpr/objectSpread.res diff --git a/src/res_core.ml b/src/res_core.ml index 412b1e47..960b4e19 100644 --- a/src/res_core.ml +++ b/src/res_core.ml @@ -112,6 +112,15 @@ Solution: directly use `concat`." let stringInterpolationInPattern = "String interpolation is not supported in pattern matching." + + let spreadInRecordDeclaration = + "A record type declaration doesn't support the ... spread. Only an object (with quoted field names) does." + + let objectQuotedFieldName name = + "An object type declaration needs quoted field names. Did you mean \"" ^ name ^ "\"?" + + let forbiddenInlineRecordDeclaration = + "An inline record type declaration is only allowed in a variant constructor's declaration" end @@ -3834,6 +3843,11 @@ and parseRecordOrObjectType ~attrs p = | Dot -> Parser.next p; Asttypes.Closed | _ -> Asttypes.Closed in + let () = match p.token with + | Lident _ -> + Parser.err p (Diagnostics.message ErrorMessages.forbiddenInlineRecordDeclaration) + | _ -> () + in let fields = parseCommaDelimitedRegion ~grammar:Grammar.StringFieldDeclarations @@ -4136,7 +4150,7 @@ and parseStringFieldDeclaration p = Some(Parsetree.Oinherit typ) | Lident name -> let nameLoc = mkLoc p.startPos p.endPos in - Parser.err p (Diagnostics.message "An inline record type declaration is only allowed in a variant constructor's declaration"); + Parser.err p (Diagnostics.message (ErrorMessages.objectQuotedFieldName name)); Parser.next p; let fieldName = Location.mkloc name nameLoc in Parser.expect ~grammar:Grammar.TypeExpression Colon p; @@ -4261,10 +4275,18 @@ and parseConstrDeclArgs p = Parser.expect Rparen p; Parsetree.Pcstr_tuple (typ::moreArgs) | DotDotDot -> + let dotdotdotStart = p.startPos in + let dotdotdotEnd = p.endPos in (* start of object type spreading, e.g. `User({...a, "u": int})` *) Parser.next p; let typ = parseTypExpr p in Parser.expect Comma p; + let () = match p.token with + | Lident _ -> + Parser.err ~startPos:dotdotdotStart ~endPos:dotdotdotEnd p + (Diagnostics.message ErrorMessages.spreadInRecordDeclaration) + | _ -> () + in let fields = (Parsetree.Oinherit typ)::( parseCommaDelimitedRegion @@ -4667,10 +4689,18 @@ and parseRecordOrObjectDecl p = let typ = parseArrowTypeRest ~es6Arrow:true ~startPos typ p in (Some typ, Asttypes.Public, Parsetree.Ptype_abstract) | DotDotDot -> + let dotdotdotStart = p.startPos in + let dotdotdotEnd = p.endPos in (* start of object type spreading, e.g. `type u = {...a, "u": int}` *) Parser.next p; let typ = parseTypExpr p in Parser.expect Comma p; + let () = match p.token with + | Lident _ -> + Parser.err ~startPos:dotdotdotStart ~endPos:dotdotdotEnd p + (Diagnostics.message ErrorMessages.spreadInRecordDeclaration) + | _ -> () + in let fields = (Parsetree.Oinherit typ)::( parseCommaDelimitedRegion diff --git a/tests/parsing/errors/typexpr/__snapshots__/parse.spec.js.snap b/tests/parsing/errors/typexpr/__snapshots__/parse.spec.js.snap index af65d7ae..682bebbe 100644 --- a/tests/parsing/errors/typexpr/__snapshots__/parse.spec.js.snap +++ b/tests/parsing/errors/typexpr/__snapshots__/parse.spec.js.snap @@ -219,6 +219,71 @@ external printName : name:((unit)[@ns.namedArgLoc ]) -> unit = \\"printName\\" I'm not sure what to parse here when looking at \\"?\\". +========================================================" +`; + +exports[`objectSpread.res 1`] = ` +"=====Parsetree========================================== +type nonrec u = < a ;u: int > +type nonrec u = private < a ;u: int > +type nonrec x = + | Type of < a ;u: int > +type nonrec u = < a ;u: int ;v: int > +let f (x : < a: int ;b: int > ) = () +=====Errors============================================= + + Syntax error! + parsing/errors/typexpr/objectSpread.res:1:11-13 + 1 │ type u = {...a, u: int} + 2 │ + 3 │ type u = private {...a, u: int} + + A record type declaration doesn't support the ... spread. Only an object (with quoted field names) does. + + + Syntax error! + parsing/errors/typexpr/objectSpread.res:3:19-21 + 1 │ type u = {...a, u: int} + 2 │ + 3 │ type u = private {...a, u: int} + 4 │ + 5 │ type x = Type({...a, u: int}) + + A record type declaration doesn't support the ... spread. Only an object (with quoted field names) does. + + + Syntax error! + parsing/errors/typexpr/objectSpread.res:5:16-18 + 3 │ type u = private {...a, u: int} + 4 │ + 5 │ type x = Type({...a, u: int}) + 6 │ + 7 │ type u = {...a, \\"u\\": int, v: int} + + A record type declaration doesn't support the ... spread. Only an object (with quoted field names) does. + + + Syntax error! + parsing/errors/typexpr/objectSpread.res:7:27 + 5 │ type x = Type({...a, u: int}) + 6 │ + 7 │ type u = {...a, \\"u\\": int, v: int} + 8 │ + 9 │ let f = (x: {a: int, b: int}) => () + + An object type declaration needs quoted field names. Did you mean \\"v\\"? + + + Syntax error! + parsing/errors/typexpr/objectSpread.res:9:14 + 7 │ type u = {...a, \\"u\\": int, v: int} + 8 │ + 9 │ let f = (x: {a: int, b: int}) => () + 10 │ + + An inline record type declaration is only allowed in a variant constructor's declaration + + ========================================================" `; diff --git a/tests/parsing/errors/typexpr/objectSpread.res b/tests/parsing/errors/typexpr/objectSpread.res new file mode 100644 index 00000000..c4e7db71 --- /dev/null +++ b/tests/parsing/errors/typexpr/objectSpread.res @@ -0,0 +1,9 @@ +type u = {...a, u: int} + +type u = private {...a, u: int} + +type x = Type({...a, u: int}) + +type u = {...a, "u": int, v: int} + +let f = (x: {a: int, b: int}) => () From bacf061a53c2ed2af276d83c5704438b49d50f35 Mon Sep 17 00:00:00 2001 From: Iwan Date: Mon, 15 Mar 2021 07:55:26 +0100 Subject: [PATCH 5/8] Fix printing of open objects with spread. --- src/res_printer.ml | 9 ++++++++- .../reason/__snapshots__/render.spec.js.snap | 8 ++++++++ tests/conversion/reason/object.ml | 4 ++++ .../typexpr/__snapshots__/render.spec.js.snap | 15 +++++++++++++++ tests/printer/typexpr/objectTypeSpreading.res | 5 +++++ 5 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 tests/conversion/reason/object.ml diff --git a/src/res_printer.ml b/src/res_printer.ml index a3547470..83ea99d8 100644 --- a/src/res_printer.ml +++ b/src/res_printer.ml @@ -1712,7 +1712,14 @@ and printObject ~inline fields openFlag cmtTbl = Doc.lbrace; (match openFlag with | Asttypes.Closed -> Doc.nil - | Open -> Doc.dotdot); + | Open -> + begin match fields with + (* handle `type t = {.. ...objType, "x": int}` + * .. and ... should have a space in between *) + | (Oinherit _)::_ -> Doc.text ".. " + | _ -> Doc.dotdot + end + ); Doc.indent ( Doc.concat [ Doc.softLine; diff --git a/tests/conversion/reason/__snapshots__/render.spec.js.snap b/tests/conversion/reason/__snapshots__/render.spec.js.snap index 77c346e2..113140cb 100644 --- a/tests/conversion/reason/__snapshots__/render.spec.js.snap +++ b/tests/conversion/reason/__snapshots__/render.spec.js.snap @@ -1430,6 +1430,14 @@ let make = ( " `; +exports[`object.ml 1`] = ` +"type hi = {\\"z\\": int} +type u<'a> = {.. ...hi, \\"x\\": int, \\"y\\": int} as 'a +type u1<'a> = {.. ...hi} as 'a +type u2<'a> = {.. ...hi, ...hi, \\"y\\": int, ...hi} as 'a +" +`; + exports[`openPattern.re 1`] = ` "let {T.a: a} = a() let [Color.Blue] = a() diff --git a/tests/conversion/reason/object.ml b/tests/conversion/reason/object.ml new file mode 100644 index 00000000..62099596 --- /dev/null +++ b/tests/conversion/reason/object.ml @@ -0,0 +1,4 @@ +type hi = < z : int > +type 'a u = < hi ; x : int ; y : int; .. > as 'a +type 'a u1 = < hi; .. > as 'a +type 'a u2 = < hi ; hi; y : int ; hi; .. > as 'a diff --git a/tests/printer/typexpr/__snapshots__/render.spec.js.snap b/tests/printer/typexpr/__snapshots__/render.spec.js.snap index b53c5cc6..2db99f9b 100644 --- a/tests/printer/typexpr/__snapshots__/render.spec.js.snap +++ b/tests/printer/typexpr/__snapshots__/render.spec.js.snap @@ -456,6 +456,21 @@ type polyTaggedUser2 = [ {...salary, \\"taxFraud\\": bool}, ) ] + +// notice .. and ..., they should have a space +type u<'a> = {.. ...hi} as 'a +type u<'a> = {.. + ...hi, + \\"superLongFieldName\\": string, + \\"superLongFieldName22222222222\\": int, +} as 'a +type u<'a> = {.. + ...hi, + \\"superLongFieldName\\": string, + ...hi, + \\"superLongFieldName22222222222\\": int, + ...hi, +} as 'a " `; diff --git a/tests/printer/typexpr/objectTypeSpreading.res b/tests/printer/typexpr/objectTypeSpreading.res index 216d4000..8e2d2e05 100644 --- a/tests/printer/typexpr/objectTypeSpreading.res +++ b/tests/printer/typexpr/objectTypeSpreading.res @@ -38,3 +38,8 @@ type polyTaggedUser2 = [ | #User({...user, "age": int}) | #Ceo({...user, "age": int, "direction": bool}, {...salary, "taxFraud": bool}) ] + +// notice .. and ..., they should have a space +type u<'a> = {.. ...hi} as 'a +type u<'a> = {.. ...hi, "superLongFieldName": string, "superLongFieldName22222222222": int} as 'a +type u<'a> = {.. ...hi, "superLongFieldName": string, ...hi, "superLongFieldName22222222222": int, ...hi} as 'a From 14bcd866ed6efacd5eacac327731c27a3e0b585a Mon Sep 17 00:00:00 2001 From: Iwan Date: Tue, 23 Mar 2021 18:52:56 +0100 Subject: [PATCH 6/8] Implement error message for ambiguous record spread or object type spread. --- src/res_core.ml | 19 +++++++++-- .../other/__snapshots__/parse.spec.js.snap | 33 ++++++++++++++++--- tests/parsing/errors/other/spread.res | 3 ++ 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/res_core.ml b/src/res_core.ml index 960b4e19..9d9810c8 100644 --- a/src/res_core.ml +++ b/src/res_core.ml @@ -121,6 +121,9 @@ Solution: directly use `concat`." let forbiddenInlineRecordDeclaration = "An inline record type declaration is only allowed in a variant constructor's declaration" + + let ambiguousRecordSpread = + "The … spread is only supported for record value update and object type declaration." end @@ -4280,7 +4283,13 @@ and parseConstrDeclArgs p = (* start of object type spreading, e.g. `User({...a, "u": int})` *) Parser.next p; let typ = parseTypExpr p in - Parser.expect Comma p; + let () = match p.token with + | Rbrace -> + Parser.err ~startPos:dotdotdotStart ~endPos:dotdotdotEnd p + (Diagnostics.message ErrorMessages.ambiguousRecordSpread); + Parser.next p; + | _ -> Parser.expect Comma p + in let () = match p.token with | Lident _ -> Parser.err ~startPos:dotdotdotStart ~endPos:dotdotdotEnd p @@ -4694,7 +4703,13 @@ and parseRecordOrObjectDecl p = (* start of object type spreading, e.g. `type u = {...a, "u": int}` *) Parser.next p; let typ = parseTypExpr p in - Parser.expect Comma p; + let () = match p.token with + | Rbrace -> + Parser.err ~startPos:dotdotdotStart ~endPos:dotdotdotEnd p + (Diagnostics.message ErrorMessages.ambiguousRecordSpread); + Parser.next p; + | _ -> Parser.expect Comma p + in let () = match p.token with | Lident _ -> Parser.err ~startPos:dotdotdotStart ~endPos:dotdotdotEnd p diff --git a/tests/parsing/errors/other/__snapshots__/parse.spec.js.snap b/tests/parsing/errors/other/__snapshots__/parse.spec.js.snap index 1beca263..6dcaf4b1 100644 --- a/tests/parsing/errors/other/__snapshots__/parse.spec.js.snap +++ b/tests/parsing/errors/other/__snapshots__/parse.spec.js.snap @@ -166,6 +166,9 @@ let record = { x with y } let { x; y } = myRecord let myList = x :: y let x::y = myList +type nonrec t = < a > +type nonrec t = + | Foo of < a > =====Errors============================================= Syntax error! @@ -216,14 +219,36 @@ Solution: you need to pull out each field you want explicitly. Syntax error! parsing/errors/other/spread.res:8:13-22 - 6 │ - 7 │ let myList = list{...x, ...y} - 8 │ let list{...x, ...y} = myList - 9 │ + 6 │ + 7 │ let myList = list{...x, ...y} + 8 │ let list{...x, ...y} = myList + 9 │ + 10 │ type t = {...a} List pattern matches only supports one \`...\` spread, at the end. Explanation: a list spread at the tail is efficient, but a spread in the middle would create new list[s]; out of performance concern, our pattern matching currently guarantees to never create new intermediate data. + Syntax error! + parsing/errors/other/spread.res:10:11-13 + 8 │ let list{...x, ...y} = myList + 9 │ + 10 │ type t = {...a} + 11 │ type t = Foo({...a}) + 12 │ + + The … spread is only supported for record value update and object type declaration. + + + Syntax error! + parsing/errors/other/spread.res:11:15-17 + 9 │ + 10 │ type t = {...a} + 11 │ type t = Foo({...a}) + 12 │ + + The … spread is only supported for record value update and object type declaration. + + ========================================================" `; diff --git a/tests/parsing/errors/other/spread.res b/tests/parsing/errors/other/spread.res index 39be7c29..5ed52108 100644 --- a/tests/parsing/errors/other/spread.res +++ b/tests/parsing/errors/other/spread.res @@ -6,3 +6,6 @@ let {...x, ...y} = myRecord let myList = list{...x, ...y} let list{...x, ...y} = myList + +type t = {...a} +type t = Foo({...a}) From 1bb58dd002a5f437e1096cd7ce42da7ad28f26a3 Mon Sep 17 00:00:00 2001 From: Iwan Date: Thu, 25 Mar 2021 07:00:53 +0100 Subject: [PATCH 7/8] Improve spreading without extra fields error message. --- src/res_core.ml | 18 ++++-- .../conversion/reason/expected/object.ml.txt | 4 ++ .../errors/other/expected/spread.res.txt | 50 +++++++++++++-- tests/parsing/errors/other/spread.res | 1 + .../typexpr/expected/objectSpread.res.txt | 63 +++++++++++++++++++ .../expected/objectTypeSpreading.res.txt | 39 ++++++++++++ .../expected/objectTypeSpreading.res.txt | 50 +++++++++++++++ 7 files changed, 216 insertions(+), 9 deletions(-) create mode 100644 tests/conversion/reason/expected/object.ml.txt create mode 100644 tests/parsing/errors/typexpr/expected/objectSpread.res.txt create mode 100644 tests/parsing/grammar/typexpr/expected/objectTypeSpreading.res.txt create mode 100644 tests/printer/typexpr/expected/objectTypeSpreading.res.txt diff --git a/src/res_core.ml b/src/res_core.ml index 9d9810c8..9475700c 100644 --- a/src/res_core.ml +++ b/src/res_core.ml @@ -122,8 +122,8 @@ Solution: directly use `concat`." let forbiddenInlineRecordDeclaration = "An inline record type declaration is only allowed in a variant constructor's declaration" - let ambiguousRecordSpread = - "The … spread is only supported for record value update and object type declaration." + let sameTypeSpread = + "You're using a ... spread without extra fields. This is the same type." end @@ -3851,6 +3851,7 @@ and parseRecordOrObjectType ~attrs p = Parser.err p (Diagnostics.message ErrorMessages.forbiddenInlineRecordDeclaration) | _ -> () in + let startFirstField = p.startPos in let fields = parseCommaDelimitedRegion ~grammar:Grammar.StringFieldDeclarations @@ -3858,6 +3859,13 @@ and parseRecordOrObjectType ~attrs p = ~f:parseStringFieldDeclaration p in + let () = match fields with + | [Parsetree.Oinherit {ptyp_loc}] -> + (* {...x}, spread without extra fields *) + Parser.err p ~startPos:startFirstField ~endPos:ptyp_loc.loc_end + (Diagnostics.message ErrorMessages.sameTypeSpread) + | _ -> () + in Parser.expect Rbrace p; let loc = mkLoc startPos p.prevEndPos in Ast_helper.Typ.object_ ~loc ~attrs fields closedFlag @@ -4285,8 +4293,9 @@ and parseConstrDeclArgs p = let typ = parseTypExpr p in let () = match p.token with | Rbrace -> + (* {...x}, spread without extra fields *) Parser.err ~startPos:dotdotdotStart ~endPos:dotdotdotEnd p - (Diagnostics.message ErrorMessages.ambiguousRecordSpread); + (Diagnostics.message ErrorMessages.sameTypeSpread); Parser.next p; | _ -> Parser.expect Comma p in @@ -4705,8 +4714,9 @@ and parseRecordOrObjectDecl p = let typ = parseTypExpr p in let () = match p.token with | Rbrace -> + (* {...x}, spread without extra fields *) Parser.err ~startPos:dotdotdotStart ~endPos:dotdotdotEnd p - (Diagnostics.message ErrorMessages.ambiguousRecordSpread); + (Diagnostics.message ErrorMessages.sameTypeSpread); Parser.next p; | _ -> Parser.expect Comma p in diff --git a/tests/conversion/reason/expected/object.ml.txt b/tests/conversion/reason/expected/object.ml.txt new file mode 100644 index 00000000..3afee67d --- /dev/null +++ b/tests/conversion/reason/expected/object.ml.txt @@ -0,0 +1,4 @@ +type hi = {"z": int} +type u<'a> = {.. ...hi, "x": int, "y": int} as 'a +type u1<'a> = {.. ...hi} as 'a +type u2<'a> = {.. ...hi, ...hi, "y": int, ...hi} as 'a diff --git a/tests/parsing/errors/other/expected/spread.res.txt b/tests/parsing/errors/other/expected/spread.res.txt index 02e8476a..cfddb204 100644 --- a/tests/parsing/errors/other/expected/spread.res.txt +++ b/tests/parsing/errors/other/expected/spread.res.txt @@ -52,17 +52,57 @@ Solution: you need to pull out each field you want explicitly. Syntax error! tests/parsing/errors/other/spread.res:8:13-22 - 6 │ - 7 │ let myList = list{...x, ...y} - 8 │ let list{...x, ...y} = myList - 9 │ + 6 │ + 7 │ let myList = list{...x, ...y} + 8 │ let list{...x, ...y} = myList + 9 │ + 10 │ type t = {...a} List pattern matches only supports one `...` spread, at the end. Explanation: a list spread at the tail is efficient, but a spread in the middle would create new list[s]; out of performance concern, our pattern matching currently guarantees to never create new intermediate data. + + Syntax error! + tests/parsing/errors/other/spread.res:10:11-13 + + 8 │ let list{...x, ...y} = myList + 9 │ + 10 │ type t = {...a} + 11 │ type t = Foo({...a}) + 12 │ type t = option + + You're using a ... spread without extra fields. This is the same type. + + + Syntax error! + tests/parsing/errors/other/spread.res:11:15-17 + + 9 │ + 10 │ type t = {...a} + 11 │ type t = Foo({...a}) + 12 │ type t = option + 13 │ + + You're using a ... spread without extra fields. This is the same type. + + + Syntax error! + tests/parsing/errors/other/spread.res:12:23-26 + + 10 │ type t = {...a} + 11 │ type t = Foo({...a}) + 12 │ type t = option + 13 │ + + You're using a ... spread without extra fields. This is the same type. + let arr = [|x;y|] let [|arr;_|] = [|1;2;3|] let record = { x with y } let { x; y } = myRecord let myList = x :: y -let x::y = myList \ No newline at end of file +let x::y = myList +type nonrec t = < a > +type nonrec t = + | Foo of < a > +type nonrec t = (foo, < x > ) option \ No newline at end of file diff --git a/tests/parsing/errors/other/spread.res b/tests/parsing/errors/other/spread.res index 5ed52108..e30a6129 100644 --- a/tests/parsing/errors/other/spread.res +++ b/tests/parsing/errors/other/spread.res @@ -9,3 +9,4 @@ let list{...x, ...y} = myList type t = {...a} type t = Foo({...a}) +type t = option diff --git a/tests/parsing/errors/typexpr/expected/objectSpread.res.txt b/tests/parsing/errors/typexpr/expected/objectSpread.res.txt new file mode 100644 index 00000000..373996a4 --- /dev/null +++ b/tests/parsing/errors/typexpr/expected/objectSpread.res.txt @@ -0,0 +1,63 @@ + + Syntax error! + tests/parsing/errors/typexpr/objectSpread.res:1:11-13 + + 1 │ type u = {...a, u: int} + 2 │ + 3 │ type u = private {...a, u: int} + + A record type declaration doesn't support the ... spread. Only an object (with quoted field names) does. + + + Syntax error! + tests/parsing/errors/typexpr/objectSpread.res:3:19-21 + + 1 │ type u = {...a, u: int} + 2 │ + 3 │ type u = private {...a, u: int} + 4 │ + 5 │ type x = Type({...a, u: int}) + + A record type declaration doesn't support the ... spread. Only an object (with quoted field names) does. + + + Syntax error! + tests/parsing/errors/typexpr/objectSpread.res:5:16-18 + + 3 │ type u = private {...a, u: int} + 4 │ + 5 │ type x = Type({...a, u: int}) + 6 │ + 7 │ type u = {...a, "u": int, v: int} + + A record type declaration doesn't support the ... spread. Only an object (with quoted field names) does. + + + Syntax error! + tests/parsing/errors/typexpr/objectSpread.res:7:27 + + 5 │ type x = Type({...a, u: int}) + 6 │ + 7 │ type u = {...a, "u": int, v: int} + 8 │ + 9 │ let f = (x: {a: int, b: int}) => () + + An object type declaration needs quoted field names. Did you mean "v"? + + + Syntax error! + tests/parsing/errors/typexpr/objectSpread.res:9:14 + + 7 │ type u = {...a, "u": int, v: int} + 8 │ + 9 │ let f = (x: {a: int, b: int}) => () + 10 │ + + An inline record type declaration is only allowed in a variant constructor's declaration + +type nonrec u = < a ;u: int > +type nonrec u = private < a ;u: int > +type nonrec x = + | Type of < a ;u: int > +type nonrec u = < a ;u: int ;v: int > +let f (x : < a: int ;b: int > ) = () \ No newline at end of file diff --git a/tests/parsing/grammar/typexpr/expected/objectTypeSpreading.res.txt b/tests/parsing/grammar/typexpr/expected/objectTypeSpreading.res.txt new file mode 100644 index 00000000..4ac552a2 --- /dev/null +++ b/tests/parsing/grammar/typexpr/expected/objectTypeSpreading.res.txt @@ -0,0 +1,39 @@ +type nonrec a = < x: int > +type nonrec u = < a ;u: int > +type nonrec v = < v: int ;a > +type nonrec w = < j: int ;a ;k: int ;v > +type nonrec t = < a ;u: int > as 'a +type nonrec t = < a ;u: int > -> unit +type nonrec t = (< a ;u: int > as 'a) -> unit +type nonrec t = < a ;u: int > -> < a ;v: int > -> unit +type nonrec user = < name: string > +let (steve : < user ;age: int > ) = [%obj { name = "Steve"; age = 30 }] +let steve = ([%obj { name = "Steve"; age = 30 }] : < user ;age: int > ) +let steve = ((([%obj { name = "Steve"; age = 30 }] : < user ;age: int > )) + [@ns.braces ]) +let printFullUser (steve : < user ;age: int > ) = Js.log steve +let printFullUser ~user:(((user : < user ;age: int > ))[@ns.namedArgLoc ]) + = Js.log steve +let printFullUser ~user:(((user : < user ;age: int > ))[@ns.namedArgLoc ]) + = Js.log steve +let printFullUser ?user:(((user)[@ns.namedArgLoc ])= + (steve : < user ;age: int > )) = Js.log steve +external steve : < user ;age: int > = "steve"[@@val ] +let makeCeoOf30yearsOld name = + ([%obj { name; age = 30 }] : < user ;age: int > ) +type nonrec optionalUser = < user ;age: int > option +type nonrec optionalTupleUser = + (< user ;age: int > * < user ;age: int > ) option +type nonrec constrUser = + (< user ;age: int > , < user ;age: int > ) myTypeConstructor +type nonrec taggedUser = + | User of < user ;age: int > + | Ceo of < user ;age: int ;direction: bool > * + < salary ;taxFraud: bool > +type nonrec polyTaggedUser = [ `User of < user ;age: int > ] +type nonrec polyTaggedUser2 = + [ `User of < user ;age: int > + | `Ceo of + (< user ;age: int ;direction: bool > * + < salary ;taxFraud: bool > ) + ] \ No newline at end of file diff --git a/tests/printer/typexpr/expected/objectTypeSpreading.res.txt b/tests/printer/typexpr/expected/objectTypeSpreading.res.txt new file mode 100644 index 00000000..81446937 --- /dev/null +++ b/tests/printer/typexpr/expected/objectTypeSpreading.res.txt @@ -0,0 +1,50 @@ +type a = {"x": int} +type u = {...a, "u": int} +type v = {"v": int, ...a} +type w = {"j": int, ...a, "k": int, ...v} + +type t = {...a, "u": int} as 'a +type t = {...a, "u": int} => unit +type t = ({...a, "u": int} as 'a) => unit +type t = ({...a, "u": int}, {...a, "v": int}) => unit + +type user = {"name": string} + +let steve: {...user, "age": int} = {"name": "Steve", "age": 30} +let steve = ({"name": "Steve", "age": 30}: {...user, "age": int}) +let steve = {({"name": "Steve", "age": 30}: {...user, "age": int})} + +let printFullUser = (steve: {...user, "age": int}) => Js.log(steve) +let printFullUser = (~user: {...user, "age": int}) => Js.log(steve) +let printFullUser = (~user: {...user, "age": int}) => Js.log(steve) +let printFullUser = (~user=steve: {...user, "age": int}) => Js.log(steve) + +@val +external steve: {...user, "age": int} = "steve" + +let makeCeoOf30yearsOld = (name): {...user, "age": int} => {"name": name, "age": 30} + +type optionalUser = option<{...user, "age": int}> +type optionalTupleUser = option<({...user, "age": int}, {...user, "age": int})> +type constrUser = myTypeConstructor<{...user, "age": int}, {...user, "age": int}> + +type taggedUser = + | User({...user, "age": int}) + | Ceo({...user, "age": int, "direction": bool}, {...salary, "taxFraud": bool}) + +type polyTaggedUser = [#User({...user, "age": int})] +type polyTaggedUser2 = [ + | #User({...user, "age": int}) + | #Ceo({...user, "age": int, "direction": bool}, {...salary, "taxFraud": bool}) +] + +// notice .. and ..., they should have a space +type u<'a> = {.. ...hi} as 'a +type u<'a> = {.. ...hi, "superLongFieldName": string, "superLongFieldName22222222222": int} as 'a +type u<'a> = {.. + ...hi, + "superLongFieldName": string, + ...hi, + "superLongFieldName22222222222": int, + ...hi, +} as 'a From 171c35eb60af22f7b18f36a49971c81c2331c547 Mon Sep 17 00:00:00 2001 From: Iwan Date: Thu, 25 Mar 2021 19:20:09 +0100 Subject: [PATCH 8/8] Update object method parsing and printer for ReScript 9 Fixes https://github.com/rescript-lang/syntax/issues/311 `obj["say"]` was currently parsed as `obj##say` (Pexp_apply with ##) We now parse this straight into `obj#say`(Pexp_send) --- src/res_ast_conversion.ml | 24 ++++++++---------- src/res_core.ml | 4 +-- src/res_printer.ml | 22 ++++++++++++++-- .../reason/__snapshots__/render.spec.js.snap | 11 ++++++++ tests/conversion/reason/jsObject.re | 12 +++++++++ .../other/__snapshots__/parse.spec.js.snap | 22 ++++++++++++---- .../other/expected/breadcrumbs170.res.txt | 2 +- .../__snapshots__/parse.spec.js.snap | 25 +++++++++---------- .../grammar/expressions/expected/jsx.res.txt | 11 ++++---- .../expressions/expected/primary.res.txt | 14 +++++------ 10 files changed, 97 insertions(+), 50 deletions(-) diff --git a/src/res_ast_conversion.ml b/src/res_ast_conversion.ml index 9aacff35..aea8c210 100644 --- a/src/res_ast_conversion.ml +++ b/src/res_ast_conversion.ml @@ -421,7 +421,7 @@ let normalize = pexp_attributes = []; pexp_desc = Pexp_ident (Location.mknoloc (Longident.Lident "x")) }, - (default_mapper.cases mapper cases) + (mapper.cases mapper cases) ) } @@ -435,21 +435,19 @@ let normalize = { pexp_loc = expr.pexp_loc; pexp_attributes = expr.pexp_attributes; - pexp_desc = Pexp_field (operand, (Location.mknoloc (Longident.Lident "contents"))) + pexp_desc = Pexp_field (mapper.expr mapper operand, (Location.mknoloc (Longident.Lident "contents"))) } | Pexp_apply ( - {pexp_desc = Pexp_ident {txt = Longident.Lident "##"}} as op, - [Asttypes.Nolabel, lhs; Nolabel, ({pexp_desc = Pexp_constant (Pconst_string (txt, None))} as stringExpr)] + {pexp_desc = Pexp_ident {txt = Longident.Lident "##"}}, + [ + Asttypes.Nolabel, lhs; Nolabel, + ({pexp_desc = Pexp_constant (Pconst_string (txt, None)) | (Pexp_ident ({txt = Longident.Lident txt})); pexp_loc = labelLoc})] ) -> - let ident = { - Parsetree.pexp_loc = stringExpr.pexp_loc; - pexp_attributes = []; - pexp_desc = Pexp_ident (Location.mkloc (Longident.Lident txt) stringExpr.pexp_loc) - } in + let label = Location.mkloc txt labelLoc in { pexp_loc = expr.pexp_loc; pexp_attributes = expr.pexp_attributes; - pexp_desc = Pexp_apply (op, [Asttypes.Nolabel, lhs; Nolabel, ident]) + pexp_desc = Pexp_send (mapper.expr mapper lhs, label) } | Pexp_match ( condition, @@ -461,9 +459,9 @@ let normalize = let ternaryMarker = (Location.mknoloc "ns.ternary", Parsetree.PStr []) in {Parsetree.pexp_loc = expr.pexp_loc; pexp_desc = Pexp_ifthenelse ( - default_mapper.expr mapper condition, - default_mapper.expr mapper thenExpr, - (Some (default_mapper.expr mapper elseExpr)) + mapper.expr mapper condition, + mapper.expr mapper thenExpr, + (Some (mapper.expr mapper elseExpr)) ); pexp_attributes = ternaryMarker::expr.pexp_attributes; } diff --git a/src/res_core.ml b/src/res_core.ml index 9475700c..21e191e0 100644 --- a/src/res_core.ml +++ b/src/res_core.ml @@ -1955,9 +1955,7 @@ and parseBracketAccess p expr startPos = let e = let identLoc = mkLoc stringStart stringEnd in let loc = mkLoc lbracket rbracket in - Ast_helper.Exp.apply ~loc - (Ast_helper.Exp.ident ~loc (Location.mkloc (Longident.Lident "##") loc)) - [Nolabel, expr; Nolabel, (Ast_helper.Exp.ident ~loc:identLoc (Location.mkloc (Longident.Lident s) identLoc))] + Ast_helper.Exp.send ~loc expr (Location.mkloc s identLoc) in let e = parsePrimaryExpr ~operand:e p in let equalStart = p.startPos in diff --git a/src/res_printer.ml b/src/res_printer.ml index 83ea99d8..225230ba 100644 --- a/src/res_printer.ml +++ b/src/res_printer.ml @@ -3137,8 +3137,26 @@ and printExpression (e : Parsetree.expression) cmtTbl = Doc.concat [Doc.text ": "; printTypExpr typ1 cmtTbl] in Doc.concat [Doc.lparen; docExpr; ofType; Doc.text " :> "; docTyp; Doc.rparen] - | Pexp_send _ -> - Doc.text "Pexp_send not impemented in printer" + | Pexp_send (parentExpr, label) -> + let parentDoc = + let doc = printExpressionWithComments parentExpr cmtTbl in + match Parens.unaryExprOperand parentExpr with + | Parens.Parenthesized -> addParens doc + | Braced braces -> printBraces doc parentExpr braces + | Nothing -> doc + in + let member = + let memberDoc = printComments (Doc.text label.txt) cmtTbl label.loc in + Doc.concat [Doc.text "\""; memberDoc; Doc.text "\""] + in + Doc.group ( + Doc.concat [ + parentDoc; + Doc.lbracket; + member; + Doc.rbracket; + ] + ) | Pexp_new _ -> Doc.text "Pexp_new not impemented in printer" | Pexp_setinstvar _ -> diff --git a/tests/conversion/reason/__snapshots__/render.spec.js.snap b/tests/conversion/reason/__snapshots__/render.spec.js.snap index 113140cb..558acefa 100644 --- a/tests/conversion/reason/__snapshots__/render.spec.js.snap +++ b/tests/conversion/reason/__snapshots__/render.spec.js.snap @@ -1285,6 +1285,17 @@ type propField<'a> = Js.nullable<{..} as 'a> type propField<'a> = {\\"a\\": b} type propField<'a> = {..\\"a\\": b} type propField<'a> = {\\"a\\": {\\"b\\": c}} + +user[\\"address\\"] +user[\\"address\\"][\\"street\\"] +user[\\"address\\"][\\"street\\"][\\"log\\"] + +user[\\"address\\"] = \\"Avenue 1\\" +user[\\"address\\"][\\"street\\"] = \\"Avenue\\" +user[\\"address\\"][\\"street\\"][\\"number\\"] = \\"1\\" + +school[\\"print\\"](direction[\\"name\\"], studentHead[\\"name\\"]) +city[\\"getSchool\\"]()[\\"print\\"](direction[\\"name\\"], studentHead[\\"name\\"]) " `; diff --git a/tests/conversion/reason/jsObject.re b/tests/conversion/reason/jsObject.re index 48aa165b..9d2eeefe 100644 --- a/tests/conversion/reason/jsObject.re +++ b/tests/conversion/reason/jsObject.re @@ -13,3 +13,15 @@ type propField('a) = Js.nullable(Js.t({..} as 'a)) type propField('a) = {. "a": b} type propField('a) = {.. "a": b} type propField('a) = Js.t(Js.t({. "a": Js.t({. "b": c})})) + +user##address; +user##address##street; +user##address##street##log; + +user##address #= "Avenue 1"; +user##address##street #= "Avenue" ; +user##address##street##number #= "1"; + +school##print(direction##name, studentHead##name); +(city##getSchool())##print(direction##name, studentHead##name); + diff --git a/tests/parsing/errors/other/__snapshots__/parse.spec.js.snap b/tests/parsing/errors/other/__snapshots__/parse.spec.js.snap index 6dcaf4b1..c1e56b30 100644 --- a/tests/parsing/errors/other/__snapshots__/parse.spec.js.snap +++ b/tests/parsing/errors/other/__snapshots__/parse.spec.js.snap @@ -3,7 +3,7 @@ exports[`breadcrumbs170.res 1`] = ` "=====Parsetree========================================== let l = (Some [1; 2; 3]) |> Obj.magic -module M = struct ;;match l with | None -> [] | Some l -> l ## prop end +module M = struct ;;match l with | None -> [] | Some l -> l#prop end ;;from ;;now ;;on @@ -169,6 +169,7 @@ let x::y = myList type nonrec t = < a > type nonrec t = | Foo of < a > +type nonrec t = (foo, < x > ) option =====Errors============================================= Syntax error! @@ -235,9 +236,9 @@ Explanation: a list spread at the tail is efficient, but a spread in the middle 9 │ 10 │ type t = {...a} 11 │ type t = Foo({...a}) - 12 │ + 12 │ type t = option - The … spread is only supported for record value update and object type declaration. + You're using a ... spread without extra fields. This is the same type. Syntax error! @@ -245,9 +246,20 @@ Explanation: a list spread at the tail is efficient, but a spread in the middle 9 │ 10 │ type t = {...a} 11 │ type t = Foo({...a}) - 12 │ + 12 │ type t = option + 13 │ - The … spread is only supported for record value update and object type declaration. + You're using a ... spread without extra fields. This is the same type. + + + Syntax error! + parsing/errors/other/spread.res:12:23-26 + 10 │ type t = {...a} + 11 │ type t = Foo({...a}) + 12 │ type t = option + 13 │ + + You're using a ... spread without extra fields. This is the same type. ========================================================" diff --git a/tests/parsing/errors/other/expected/breadcrumbs170.res.txt b/tests/parsing/errors/other/expected/breadcrumbs170.res.txt index 49784c45..8f950a16 100644 --- a/tests/parsing/errors/other/expected/breadcrumbs170.res.txt +++ b/tests/parsing/errors/other/expected/breadcrumbs170.res.txt @@ -11,7 +11,7 @@ I'm not sure what to parse here when looking at "}". let l = (Some [1; 2; 3]) |> Obj.magic -module M = struct ;;match l with | None -> [] | Some l -> l ## prop end +module M = struct ;;match l with | None -> [] | Some l -> l#prop end ;;from ;;now ;;on diff --git a/tests/parsing/grammar/expressions/__snapshots__/parse.spec.js.snap b/tests/parsing/grammar/expressions/__snapshots__/parse.spec.js.snap index 9f564423..72df3fa9 100644 --- a/tests/parsing/grammar/expressions/__snapshots__/parse.spec.js.snap +++ b/tests/parsing/grammar/expressions/__snapshots__/parse.spec.js.snap @@ -778,10 +778,10 @@ let icon = [@JSX ]) let _ = ((MessengerSharedPhotosAlbumViewPhotoReact.createElement - ?ref:((if (foo ## bar) == baz + ?ref:((if foo#bar == baz then Some (foooooooooooooooooooooooo setRefChild) else None)[@ns.namedArgLoc ][@ns.ternary ]) - ~key:((node ## legacy_attachment_id)[@ns.namedArgLoc ]) ~children:[] ()) + ~key:((node#legacy_attachment_id)[@ns.namedArgLoc ]) ~children:[] ()) [@JSX ]) let _ = ((Foo.createElement ~bar:((bar)[@ns.namedArgLoc ]) ~children:[] ()) [@JSX ]) @@ -792,7 +792,7 @@ let _ = [@JSX ]) let x = ((div ~children:[] ())[@JSX ]) let _ = ((div ~asd:((1)[@ns.namedArgLoc ]) ~children:[] ())[@JSX ]) -;;(foo ## bar) #= ((bar ~children:[] ())[@JSX ]) +;;foo#bar #= ((bar ~children:[] ())[@JSX ]) ;;foo #= ((bar ~children:[] ())[@JSX ]) ;;foo #= ((bar ~children:[] ())[@JSX ]) let x = [|((div ~children:[] ())[@JSX ])|] @@ -1078,12 +1078,11 @@ let _ = [@ns.braces ])] ()) [@JSX ]) let _ = - ((View.createElement ~style:((styles ## backgroundImageWrapper) + ((View.createElement ~style:((styles#backgroundImageWrapper) [@ns.namedArgLoc ]) ~children:[(((let uri = \\"/images/header-background.png\\" in ((Image.createElement ~resizeMode:((Contain) - [@ns.namedArgLoc ]) - ~style:((styles ## backgroundImage) + [@ns.namedArgLoc ]) ~style:((styles#backgroundImage) [@ns.namedArgLoc ]) ~uri:((uri)[@ns.namedArgLoc ]) ~children:[] ()) [@JSX ]))) @@ -1215,13 +1214,13 @@ let x = (arr.((x : int))).((y : int)) [@ns.namedArgLoc ]) ~b:((bArg)[@ns.namedArgLoc ]) ?c:((c) [@ns.namedArgLoc ]) ?d:((expr)[@ns.namedArgLoc ]) ;;f ~a:(((x : int))[@ns.namedArgLoc ]) ?b:(((y : int))[@ns.namedArgLoc ]) -;;connection ## platformId -;;((connection ## left) ## account) ## accountName -;;(john ## age) #= 99 -;;((john ## son) ## age) #= ((steve ## age) - 5) -;;(dict ## -) #= abc -;;(dict ## \\") #= (dict2 ## \\")" +;;connection#platformId +;;((connection#left)#account)#accountName +;;john#age #= 99 +;;(john#son)#age #= (steve#age - 5) +;;dict# + #= abc +;;dict#\\" #= dict2#\\"" `; exports[`record.res 1`] = ` diff --git a/tests/parsing/grammar/expressions/expected/jsx.res.txt b/tests/parsing/grammar/expressions/expected/jsx.res.txt index 1b394e2f..0e5836ba 100644 --- a/tests/parsing/grammar/expressions/expected/jsx.res.txt +++ b/tests/parsing/grammar/expressions/expected/jsx.res.txt @@ -220,10 +220,10 @@ let icon = [@JSX ]) let _ = ((MessengerSharedPhotosAlbumViewPhotoReact.createElement - ?ref:((if (foo ## bar) == baz + ?ref:((if foo#bar == baz then Some (foooooooooooooooooooooooo setRefChild) else None)[@ns.namedArgLoc ][@ns.ternary ]) - ~key:((node ## legacy_attachment_id)[@ns.namedArgLoc ]) ~children:[] ()) + ~key:((node#legacy_attachment_id)[@ns.namedArgLoc ]) ~children:[] ()) [@JSX ]) let _ = ((Foo.createElement ~bar:((bar)[@ns.namedArgLoc ]) ~children:[] ()) [@JSX ]) @@ -234,7 +234,7 @@ let _ = [@JSX ]) let x = ((div ~children:[] ())[@JSX ]) let _ = ((div ~asd:((1)[@ns.namedArgLoc ]) ~children:[] ())[@JSX ]) -;;(foo ## bar) #= ((bar ~children:[] ())[@JSX ]) +;;foo#bar #= ((bar ~children:[] ())[@JSX ]) ;;foo #= ((bar ~children:[] ())[@JSX ]) ;;foo #= ((bar ~children:[] ())[@JSX ]) let x = [|((div ~children:[] ())[@JSX ])|] @@ -520,12 +520,11 @@ let _ = [@ns.braces ])] ()) [@JSX ]) let _ = - ((View.createElement ~style:((styles ## backgroundImageWrapper) + ((View.createElement ~style:((styles#backgroundImageWrapper) [@ns.namedArgLoc ]) ~children:[(((let uri = "/images/header-background.png" in ((Image.createElement ~resizeMode:((Contain) - [@ns.namedArgLoc ]) - ~style:((styles ## backgroundImage) + [@ns.namedArgLoc ]) ~style:((styles#backgroundImage) [@ns.namedArgLoc ]) ~uri:((uri)[@ns.namedArgLoc ]) ~children:[] ()) [@JSX ]))) diff --git a/tests/parsing/grammar/expressions/expected/primary.res.txt b/tests/parsing/grammar/expressions/expected/primary.res.txt index 8584645c..52c22acc 100644 --- a/tests/parsing/grammar/expressions/expected/primary.res.txt +++ b/tests/parsing/grammar/expressions/expected/primary.res.txt @@ -27,10 +27,10 @@ let x = (arr.((x : int))).((y : int)) [@ns.namedArgLoc ]) ~b:((bArg)[@ns.namedArgLoc ]) ?c:((c) [@ns.namedArgLoc ]) ?d:((expr)[@ns.namedArgLoc ]) ;;f ~a:(((x : int))[@ns.namedArgLoc ]) ?b:(((y : int))[@ns.namedArgLoc ]) -;;connection ## platformId -;;((connection ## left) ## account) ## accountName -;;(john ## age) #= 99 -;;((john ## son) ## age) #= ((steve ## age) - 5) -;;(dict ## -) #= abc -;;(dict ## ") #= (dict2 ## ") \ No newline at end of file +;;connection#platformId +;;((connection#left)#account)#accountName +;;john#age #= 99 +;;(john#son)#age #= (steve#age - 5) +;;dict# + #= abc +;;dict#" #= dict2#" \ No newline at end of file