From 334882adf5bce24e5d2358151c5847333b622c91 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Mon, 18 Nov 2024 18:50:57 -0500 Subject: [PATCH] Fix complex property globs --- source/parser.hera | 6 ++- source/parser/lib.civet | 82 ++++++++++++++++++--------------------- source/parser/types.civet | 11 +++++- test/import.civet | 12 +++++- test/jsx/attr.civet | 16 ++++++++ test/object.civet | 22 +++++++++-- 6 files changed, 97 insertions(+), 52 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index 34e42a33..ba767af5 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -1735,6 +1735,10 @@ PropertyAccess } } +# Property glob that starts with "." or "?.", not a brace +ExplicitPropertyGlob + &ExplicitAccessStart PropertyGlob -> $2 + PropertyGlob # NOTE: Added shorthand obj.{a,b:c} -> {a: obj.a, c: obj.b} ( PropertyAccessModifier? OptionalDot ):dot InlineComment* BracedObjectLiteral:object -> @@ -3740,7 +3744,7 @@ MethodDefinition } # NOTE: Not adding extra validation using PropertySetParameterList # NOTE: If this node layout changes, be sure to update `convertMethodTOFunction` - MethodSignature:signature !(PropertyAccess / UnaryPostfix / NonNullAssertion) BracedBlock?:block -> + MethodSignature:signature !(PropertyAccess / ExplicitPropertyGlob / UnaryPostfix / NonNullAssertion) BracedBlock?:block -> let children = $0 let generatorPos = 0 let { modifier } = signature diff --git a/source/parser/lib.civet b/source/parser/lib.civet index 180a3e80..1430bef6 100644 --- a/source/parser/lib.civet +++ b/source/parser/lib.civet @@ -31,6 +31,7 @@ import type { MemberExpression MethodDefinition NormalCatchParameter + ObjectExpression ParenthesizedExpression Placeholder StatementNode @@ -558,13 +559,14 @@ function processCallMemberExpression(node: CallExpression | MemberExpression): A if glob?.type is "PropertyGlob" prefix .= children[...i] parts := [] - let refAssignmentComma + let ref // add ref to ensure object base evaluated only once - if prefix.length > 1 - ref := makeRef() - { refAssignmentComma } = makeRefAssignment ref, prefix - prefix = [ref] - prefix = prefix.concat(glob.dot) + if prefix.length > 1 and glob.object.properties# > 1 + ref = makeRef() + { refAssignment } := makeRefAssignment ref, prefix + // First use of prefix assigns ref + prefix = [ makeLeftHandSideExpression refAssignment ] + prefix = prefix.concat glob.dot for part of glob.object.properties if part.type is "Error" @@ -575,7 +577,7 @@ function processCallMemberExpression(node: CallExpression | MemberExpression): A type: "Error" message: "Glob pattern cannot have method definition" continue - if part.value and !["CallExpression", "MemberExpression", "Identifier"].includes(part.value.type) + if part.value and part.value.type is not in ["CallExpression", "MemberExpression", "Identifier"] as (string?)[] parts.push type: "Error" message: `Glob pattern must have call or member expression value, found ${JSON.stringify(part.value)}` @@ -591,8 +593,10 @@ function processCallMemberExpression(node: CallExpression | MemberExpression): A // Not yet needed: [name, value] = [value, name] if glob.reversed - if !suppressPrefix // Don't prefix @ shorthand + unless suppressPrefix // Don't prefix @ shorthand value = prefix.concat trimFirstSpace value + // Switch from refAssignment to ref + prefix = [ ref ] ++ glob.dot if ref? if (wValue) value.unshift(wValue) if part.type is "SpreadProperty" parts.push { @@ -603,6 +607,7 @@ function processCallMemberExpression(node: CallExpression | MemberExpression): A names: part.names children: part.children.slice(0, 2) // whitespace, ... .concat(value, part.delim) + usesRef: Boolean ref } else parts.push { @@ -619,21 +624,16 @@ function processCallMemberExpression(node: CallExpression | MemberExpression): A value part.delim // comma delimiter ] + usesRef: Boolean ref } - object: ASTNodeObject .= { + object: ObjectExpression := type: "ObjectExpression" children: [ glob.object.children.0 // { ...parts glob.object.children.-1 // whitespace and } - ], + ] properties: parts - } - if refAssignmentComma - object = makeNode - type: "ParenthesizedExpression" - children: ["(", ...refAssignmentComma, object, ")"] - expression: object if (i is children.length - 1) return object return processCallMemberExpression({ // in case there are more ...node @@ -842,44 +842,36 @@ function convertNamedImportsToObject(node, pattern?: boolean) // {foo} is equivalent to foo={foo}, and // {foo, bar: baz} is equivalent to foo={foo} and bar={baz}. // {...foo} is a special case. -function convertObjectToJSXAttributes(obj) { - const { properties } = obj - const parts = [] // JSX attributes - const rest = [] // parts that need to be in {...rest} form - for (let i = 0; i < properties.length; i++) { +function convertObjectToJSXAttributes(obj: ObjectExpression) + parts := [] // JSX attributes + rest := [] // parts that need to be in {...rest} form + for part, i of obj.properties + if part.usesRef + rest.push part + continue if (i > 0) parts.push(' ') - const part = properties[i] - switch (part.type) { - case 'Identifier': + switch part.type + when "Identifier" parts.push([part.name, '={', part.name, '}']) - break - case 'Property': - if (part.name.type is 'ComputedPropertyName') { + when "Property" + if part.name.type is "ComputedPropertyName" rest.push(part) - } else { + else parts.push([part.name, '={', trimFirstSpace(part.value), '}']) - } - break - case 'SpreadProperty': + when "SpreadProperty" parts.push(['{', part.dots, part.value, '}']) - break - case 'MethodDefinition': + when "MethodDefinition" const func = convertMethodToFunction(part) - if (func) { + if func parts.push([part.name, '={', convertMethodToFunction(part), '}']) - } else { + else rest.push(part) - } - break - default: - throw new Error(`invalid object literal type in JSX attribute: ${part.type}`) - } - } - if (rest.length) { - parts.push(['{...{', ...rest, '}}']) - } + else + throw new Error `invalid object literal type in JSX attribute: ${part.type}` + if rest# + parts.push " " if parts# and parts.-1 is not " " + parts.push(["{...{", ...rest, "}}"]) return parts -} /** * Returns a new MethodDefinition node. diff --git a/source/parser/types.civet b/source/parser/types.civet index 257dfdea..ac498bf4 100644 --- a/source/parser/types.civet +++ b/source/parser/types.civet @@ -111,6 +111,7 @@ export type OtherNode = | Placeholder | PropertyAccess | PropertyBind + | PropertyGlob | RangeExpression | ReturnTypeAnnotation | ReturnValue @@ -497,6 +498,13 @@ export type PropertyBind name: string args: ASTNode[] +export type PropertyGlob + type: "PropertyGlob" + children: Children + parent?: Parent + dot: ASTNode + object: ObjectExpression + export type Call type: "Call" children: Children @@ -848,7 +856,7 @@ export type ObjectExpression type: "ObjectExpression" children: Children names: string[] - properties: Property[] + properties: (Property | SpreadProperty | MethodDefinition | ASTError)[] parent?: Parent export type Property @@ -858,6 +866,7 @@ export type Property name: string names: string[] value: ASTNode + usesRef?: boolean export type ArrayExpression type: "ArrayExpression" diff --git a/test/import.civet b/test/import.civet index 18a06bd5..7821242a 100644 --- a/test/import.civet +++ b/test/import.civet @@ -333,12 +333,20 @@ describe "import", -> } """ + testCase """ + single dynamic import declaration expression + --- + fs := import { readFileSync } from fs + --- + const fs = {readFileSync:await import("fs").readFileSync} + """ + testCase """ dynamic import declaration expression --- fs := import { readFileSync, writeFile as wf, writeFileSync: wfs } from fs --- - let ref;const fs = (ref = await import("fs"),{readFileSync:ref.readFileSync,wf:ref.writeFile,wfs:ref.writeFileSync}) + let ref;const fs = {readFileSync:(ref = await import("fs")).readFileSync,wf:ref.writeFile,wfs:ref.writeFileSync} """ throws """ @@ -354,7 +362,7 @@ describe "import", -> --- data := import { version } from package.json with type: 'json' --- - let ref;const data = (ref = await import("package.json", {with:{type: 'json'}}),{version:ref.version}) + const data = {version:await import("package.json", {with:{type: 'json'}}).version} """ // #1307 diff --git a/test/jsx/attr.civet b/test/jsx/attr.civet index fe0f93ee..3e786314 100644 --- a/test/jsx/attr.civet +++ b/test/jsx/attr.civet @@ -107,6 +107,22 @@ describe "braced JSX attributes", -> """ + testCase """ + glob with complex left-hand side + --- + + --- + let ref; + """ + + testCase """ + glob with complex left-hand side and more + --- + + --- + let ref; + """ + testCase """ bind shorthand --- diff --git a/test/object.civet b/test/object.civet index bf58ed51..b5f0f584 100644 --- a/test/object.civet +++ b/test/object.civet @@ -1285,7 +1285,15 @@ describe "object", -> --- x.y()?.z.{a,b} --- - let ref;(ref = x.y()?.z,{a:ref.a,b:ref.b}) + let ref;({a:(ref = x.y()?.z).a,b:ref.b}) + """ + + testCase """ + no ref if single right-hand side + --- + a.b.{x} + --- + ({x:a.b.x}) """ testCase """ @@ -1356,7 +1364,7 @@ describe "object", -> --- f a.b.{x,y} --- - let ref;f((ref = a.b,{x:ref.x,y:ref.y})) + let ref;f({x:(ref = a.b).x,y:ref.y}) """ testCase """ @@ -1364,7 +1372,7 @@ describe "object", -> --- f first, a.b.{x,y}, last --- - let ref;f(first, (ref = a.b,{x:ref.x,y:ref.y}), last) + let ref;f(first, {x:(ref = a.b).x,y:ref.y}, last) """ throws """ @@ -1421,6 +1429,14 @@ describe "object", -> ({a:x.a,b:x.b, c:y.c,d:y.d}) """ + testCase """ + two inside braced object literals with complex base + --- + {x().{a,b}, y()?.{c,d}} + --- + let ref;let ref1;({a:(ref = x()).a,b:ref.b, c:(ref1 = y())?.c,d:ref1?.d}) + """ + testCase """ with reserved word keys ---