BREAKING: Change to disallow indexing KCL records/objects with strings (#6529)

* Change to disallow indexing records/objects with strings

* Update output

* Remove outdated sim test

* Fix tests
This commit is contained in:
Jonathan Tran
2025-04-28 12:08:47 -04:00
committed by GitHub
parent 719136937e
commit 2e754f2a11
26 changed files with 297 additions and 1633 deletions

View File

@ -861,8 +861,8 @@ impl Node<MemberExpression> {
};
// Check the property and object match -- e.g. ints for arrays, strs for objects.
match (object, property) {
(KclValue::Object { value: map, meta: _ }, Property::String(property)) => {
match (object, property, self.computed) {
(KclValue::Object { value: map, meta: _ }, Property::String(property), false) => {
if let Some(value) = map.get(&property) {
Ok(value.to_owned())
} else {
@ -872,7 +872,11 @@ impl Node<MemberExpression> {
}))
}
}
(KclValue::Object { .. }, p) => {
(KclValue::Object { .. }, Property::String(property), true) => Err(KclError::Semantic(KclErrorDetails {
message: format!("Cannot index object with string; use dot notation instead, e.g. `obj.{property}`"),
source_ranges: vec![self.clone().into()],
})),
(KclValue::Object { .. }, p, _) => {
let t = p.type_name();
let article = article_for(t);
Err(KclError::Semantic(KclErrorDetails {
@ -885,6 +889,7 @@ impl Node<MemberExpression> {
(
KclValue::MixedArray { value: arr, .. } | KclValue::HomArray { value: arr, .. },
Property::UInt(index),
_,
) => {
let value_of_arr = arr.get(index);
if let Some(value) = value_of_arr {
@ -896,7 +901,7 @@ impl Node<MemberExpression> {
}))
}
}
(KclValue::MixedArray { .. } | KclValue::HomArray { .. }, p) => {
(KclValue::MixedArray { .. } | KclValue::HomArray { .. }, p, _) => {
let t = p.type_name();
let article = article_for(t);
Err(KclError::Semantic(KclErrorDetails {
@ -906,10 +911,10 @@ impl Node<MemberExpression> {
source_ranges: vec![self.clone().into()],
}))
}
(KclValue::Solid { value }, Property::String(prop)) if prop == "sketch" => Ok(KclValue::Sketch {
(KclValue::Solid { value }, Property::String(prop), false) if prop == "sketch" => Ok(KclValue::Sketch {
value: Box::new(value.sketch),
}),
(KclValue::Sketch { value: sk }, Property::String(prop)) if prop == "tags" => Ok(KclValue::Object {
(KclValue::Sketch { value: sk }, Property::String(prop), false) if prop == "tags" => Ok(KclValue::Object {
meta: vec![Metadata {
source_range: SourceRange::from(self.clone()),
}],
@ -919,7 +924,7 @@ impl Node<MemberExpression> {
.map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned()))))
.collect(),
}),
(being_indexed, _) => {
(being_indexed, _, _) => {
let t = being_indexed.human_friendly_type();
let article = article_for(t);
Err(KclError::Semantic(KclErrorDetails {
@ -1919,10 +1924,11 @@ impl Property {
LiteralIdentifier::Identifier(identifier) => {
let name = &identifier.name;
if !computed {
// Treat the property as a literal
// This is dot syntax. Treat the property as a literal.
Ok(Property::String(name.to_string()))
} else {
// Actually evaluate memory to compute the property.
// This is bracket syntax. Actually evaluate memory to
// compute the property.
let prop = exec_state.stack().get(name, property_src)?;
jvalue_to_prop(prop, property_sr, name)
}
@ -1940,10 +1946,9 @@ impl Property {
}))
}
}
LiteralValue::String(s) => Ok(Property::String(s)),
_ => Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![sr],
message: "Only strings or numbers (>= 0) can be properties/indexes".to_owned(),
message: "Only numbers (>= 0) can be indexes".to_owned(),
})),
}
}

View File

@ -1515,44 +1515,6 @@ const fnBox = box(3, 6, 10)"#;
return myBox
}
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_member_of_object_with_function_brace() {
let ast = r#"fn box = (obj) => {
let myBox = startSketchOn(XY)
|> startProfile(at = obj["start"])
|> line(end = [0, obj["l"]])
|> line(end = [obj["w"], 0])
|> line(end = [0, -obj["l"]])
|> close()
|> extrude(length = obj["h"])
return myBox
}
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_member_of_object_with_function_mix_period_brace() {
let ast = r#"fn box = (obj) => {
let myBox = startSketchOn(XY)
|> startProfile(at = obj["start"])
|> line(end = [0, obj["l"]])
|> line(end = [obj["w"], 0])
|> line(end = [10 - obj["w"], -obj.l])
|> close()
|> extrude(length = obj["h"])
return myBox
}
const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
"#;
parse_execute(ast).await.unwrap();

View File

@ -1310,6 +1310,7 @@ fn member_expression_dot(i: &mut TokenSlice) -> PResult<(LiteralIdentifier, usiz
/// E.g. `people[0]` or `people[i]` or `people['adam']`
fn member_expression_subscript(i: &mut TokenSlice) -> PResult<(LiteralIdentifier, usize, bool)> {
let _ = open_bracket.parse_next(i)?;
// TODO: This should be an expression, not just a literal or identifier.
let property = alt((
literal.map(LiteralIdentifier::Literal),
nameable_identifier.map(Box::new).map(LiteralIdentifier::Identifier),

View File

@ -498,27 +498,6 @@ mod double_map_fn {
super::execute(TEST_NAME, false).await
}
}
mod property_of_object {
const TEST_NAME: &str = "property_of_object";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[tokio::test(flavor = "multi_thread")]
async fn unparse() {
super::unparse(TEST_NAME).await
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}
mod index_of_array {
const TEST_NAME: &str = "index_of_array";
@ -816,6 +795,27 @@ mod invalid_member_object_prop {
super::execute(TEST_NAME, false).await
}
}
mod invalid_member_object_using_string {
const TEST_NAME: &str = "invalid_member_object_using_string";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[tokio::test(flavor = "multi_thread")]
async fn unparse() {
super::unparse(TEST_NAME).await
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}
mod non_string_key_of_object {
const TEST_NAME: &str = "non_string_key_of_object";

View File

@ -249,36 +249,6 @@ description: Result of parsing computed_var.kcl
"type": "ExpressionStatement",
"type": "ExpressionStatement"
},
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "p",
"start": 0,
"type": "Identifier"
},
"init": {
"commentStart": 0,
"end": 0,
"raw": "\"foo\"",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": "foo"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"commentStart": 0,
"declaration": {
@ -373,7 +343,7 @@ description: Result of parsing computed_var.kcl
},
"init": {
"commentStart": 0,
"computed": true,
"computed": false,
"end": 0,
"object": {
"commentStart": 0,
@ -386,7 +356,7 @@ description: Result of parsing computed_var.kcl
"property": {
"commentStart": 0,
"end": 0,
"name": "p",
"name": "foo",
"start": 0,
"type": "Identifier",
"type": "Identifier"
@ -820,6 +790,17 @@ description: Result of parsing computed_var.kcl
}
}
],
"5": [
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
],
"6": [
{
"commentStart": 0,
@ -841,17 +822,6 @@ description: Result of parsing computed_var.kcl
"type": "newLine"
}
}
],
"8": [
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"startNodes": [

View File

@ -7,9 +7,8 @@ ten = arr[i]
assert(ten, isEqualTo = 10, error = "oops")
p = "foo"
obj = { foo = 1, bar = 0 }
one = obj[p]
one = obj.foo
assert(one, isEqualTo = 1, error = "oops")

View File

@ -117,10 +117,6 @@ description: Variables in memory after executing computed_var.kcl
}
}
},
"p": {
"type": "String",
"value": "foo"
},
"ten": {
"type": "Number",
"value": 10.0,

View File

@ -11,9 +11,8 @@ ten = arr[i]
assert(ten, isEqualTo = 10, error = "oops")
p = "foo"
obj = { foo = 1, bar = 0 }
one = obj[p]
one = obj.foo
assert(one, isEqualTo = 1, error = "oops")

View File

@ -4,8 +4,7 @@ description: Error from executing invalid_index_str.kcl
---
KCL Semantic error
× semantic: Only integers >= 0 can be used as the index of an array, but
│ you're using a string
× semantic: Only numbers (>= 0) can be indexes
╭─[2:5]
1 │ arr = [1, 2, 3]
2 │ x = arr["s"]

View File

@ -4,8 +4,7 @@ description: Error from executing invalid_member_object_prop.kcl
---
KCL Semantic error
× semantic: Only arrays and objects can be indexed, but you're trying to
│ index a boolean (true/false value)
× semantic: Only numbers (>= 0) can be indexes
╭─[2:5]
1 │ b = true
2 │ x = b["property"]

View File

@ -1,6 +1,6 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact commands property_of_object.kcl
description: Artifact commands invalid_member_object_using_string.kcl
---
[
{

View File

@ -0,0 +1,6 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Artifact graph flowchart invalid_member_object_using_string.kcl
extension: md
snapshot_kind: binary
---

View File

@ -0,0 +1,196 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of parsing invalid_member_object_using_string.kcl
---
{
"Ok": {
"body": [
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "p",
"start": 0,
"type": "Identifier"
},
"init": {
"commentStart": 0,
"end": 0,
"raw": "\"foo\"",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": "foo"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "obj",
"start": 0,
"type": "Identifier"
},
"init": {
"commentStart": 0,
"end": 0,
"properties": [
{
"commentStart": 0,
"end": 0,
"key": {
"commentStart": 0,
"end": 0,
"name": "foo",
"start": 0,
"type": "Identifier"
},
"start": 0,
"type": "ObjectProperty",
"value": {
"commentStart": 0,
"end": 0,
"raw": "1",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 1.0,
"suffix": "None"
}
}
},
{
"commentStart": 0,
"end": 0,
"key": {
"commentStart": 0,
"end": 0,
"name": "bar",
"start": 0,
"type": "Identifier"
},
"start": 0,
"type": "ObjectProperty",
"value": {
"commentStart": 0,
"end": 0,
"raw": "0",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 0.0,
"suffix": "None"
}
}
}
],
"start": 0,
"type": "ObjectExpression",
"type": "ObjectExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"commentStart": 0,
"declaration": {
"commentStart": 0,
"end": 0,
"id": {
"commentStart": 0,
"end": 0,
"name": "one",
"start": 0,
"type": "Identifier"
},
"init": {
"commentStart": 0,
"computed": true,
"end": 0,
"object": {
"commentStart": 0,
"end": 0,
"name": "obj",
"start": 0,
"type": "Identifier",
"type": "Identifier"
},
"property": {
"commentStart": 0,
"end": 0,
"name": "p",
"start": 0,
"type": "Identifier",
"type": "Identifier"
},
"start": 0,
"type": "MemberExpression",
"type": "MemberExpression"
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 0,
"kind": "const",
"preComments": [
"// Try to index with a string."
],
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"commentStart": 0,
"end": 0,
"nonCodeMeta": {
"nonCodeNodes": {},
"startNodes": [
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "blockComment",
"value": "This tests computed properties.",
"style": "line"
}
},
{
"commentStart": 0,
"end": 0,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"start": 0
}
}

View File

@ -0,0 +1,14 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Error from executing invalid_member_object_using_string.kcl
---
KCL Semantic error
× semantic: Cannot index object with string; use dot notation instead, e.g.
│ `obj.foo`
╭─[6:7]
5 │ // Try to index with a string.
6 │ one = obj[p]
· ───┬──
· ╰── tests/invalid_member_object_using_string/input.kcl
╰────

View File

@ -0,0 +1,6 @@
// This tests computed properties.
p = "foo"
obj = { foo = 1, bar = 0 }
// Try to index with a string.
one = obj[p]

View File

@ -0,0 +1,5 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Operations executed invalid_member_object_using_string.kcl
---
[]

View File

@ -0,0 +1,11 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing invalid_member_object_using_string.kcl
---
// This tests computed properties.
p = "foo"
obj = { foo = 1, bar = 0 }
// Try to index with a string.
one = obj[p]

View File

@ -2,9 +2,9 @@
source: kcl-lib/src/simulation_tests.rs
description: Error from executing object_prop_not_found.kcl
---
KCL UndefinedValue error
KCL Semantic error
× undefined value: Property 'age' not found in object
× semantic: Only numbers (>= 0) can be indexes
╭─[2:5]
1 │ obj = { }
2 │ k = obj["age"]

View File

@ -1,6 +0,0 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact graph flowchart property_of_object.kcl
extension: md
snapshot_kind: binary
---

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +0,0 @@
// This tests evaluating properties of objects.
obj = { foo = 1, bar = 0 }
// Test: the property is a literal.
one_a = obj["foo"]
assert(one_a, isLessThanOrEqual = 1, error = "Literal property lookup")
assert(one_a, isGreaterThanOrEqual = 1, error = "Literal property lookup")
// Test: the property is a variable,
// which must be evaluated before looking it up.
p = "foo"
one_b = obj[p]
assert(one_b, isLessThanOrEqual = 1, error = "Computed property lookup")
assert(one_b, isGreaterThanOrEqual = 1, error = "Computed property lookup")
// Test: multiple literal properties.
obj2 = { inner = obj }
one_c = obj2.inner["foo"]
assert(one_c, isLessThanOrEqual = 1, error = "Literal property lookup")
assert(one_c, isGreaterThanOrEqual = 1, error = "Literal property lookup")
// Test: multiple properties, mix of literal and computed.
one_d = obj2.inner[p]
assert(one_d, isLessThanOrEqual = 1, error = "Computed property lookup")
assert(one_d, isGreaterThanOrEqual = 1, error = "Computed property lookup")

View File

@ -1,5 +0,0 @@
---
source: kcl/src/simulation_tests.rs
description: Operations executed property_of_object.kcl
---
[]

View File

@ -1,129 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Variables in memory after executing property_of_object.kcl
---
{
"obj": {
"type": "Object",
"value": {
"bar": {
"type": "Number",
"value": 0.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"foo": {
"type": "Number",
"value": 1.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
}
}
},
"obj2": {
"type": "Object",
"value": {
"inner": {
"type": "Object",
"value": {
"bar": {
"type": "Number",
"value": 0.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"foo": {
"type": "Number",
"value": 1.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
}
}
}
}
},
"one_a": {
"type": "Number",
"value": 1.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"one_b": {
"type": "Number",
"value": 1.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"one_c": {
"type": "Number",
"value": 1.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"one_d": {
"type": "Number",
"value": 1.0,
"ty": {
"type": "Default",
"len": {
"type": "Mm"
},
"angle": {
"type": "Degrees"
}
}
},
"p": {
"type": "String",
"value": "foo"
}
}

View File

@ -1,44 +0,0 @@
---
source: kcl-lib/src/simulation_tests.rs
description: Result of unparsing property_of_object.kcl
---
// This tests evaluating properties of objects.
obj = { foo = 1, bar = 0 }
// Test: the property is a literal.
one_a = obj["foo"]
assert(one_a, isLessThanOrEqual = 1, error = "Literal property lookup")
assert(one_a, isGreaterThanOrEqual = 1, error = "Literal property lookup")
// Test: the property is a variable,
// which must be evaluated before looking it up.
p = "foo"
one_b = obj[p]
assert(one_b, isLessThanOrEqual = 1, error = "Computed property lookup")
assert(one_b, isGreaterThanOrEqual = 1, error = "Computed property lookup")
// Test: multiple literal properties.
obj2 = { inner = obj }
one_c = obj2.inner["foo"]
assert(one_c, isLessThanOrEqual = 1, error = "Literal property lookup")
assert(one_c, isGreaterThanOrEqual = 1, error = "Literal property lookup")
// Test: multiple properties, mix of literal and computed.
one_d = obj2.inner[p]
assert(one_d, isLessThanOrEqual = 1, error = "Computed property lookup")
assert(one_d, isGreaterThanOrEqual = 1, error = "Computed property lookup")

View File

@ -293,7 +293,7 @@ const newVar = myVar + 1`
})
})
it('execute memberExpression', async () => {
const code = ["const yo = {a: {b: '123'}}", "const myVar = yo.a['b']"].join(
const code = ["const yo = {a: {b: '123'}}", 'const myVar = yo.a.b'].join(
'\n'
)
const mem = await exe(code)