From db9951455f258bcbfb47d2894d07f71ab5df28fa Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 13 Oct 2025 16:19:02 -0700 Subject: [PATCH 1/4] Enable expressions to use index accessor for object properties --- dsc/tests/dsc_expressions.tests.ps1 | 9 +++- grammars/tree-sitter-dscexpression/grammar.js | 3 +- .../test/corpus/invalid_expressions.txt | 9 ++++ .../test/corpus/valid_expressions.txt | 18 ++++++++ lib/dsc-lib/locales/en-us.toml | 14 +++--- lib/dsc-lib/src/parser/expressions.rs | 43 ++++++++++++++++--- 6 files changed, 81 insertions(+), 15 deletions(-) diff --git a/dsc/tests/dsc_expressions.tests.ps1 b/dsc/tests/dsc_expressions.tests.ps1 index 34d4b3e11..a537c0fdd 100644 --- a/dsc/tests/dsc_expressions.tests.ps1 +++ b/dsc/tests/dsc_expressions.tests.ps1 @@ -12,6 +12,11 @@ Describe 'Expressions tests' { @{ text = "[parameters('test').objectArray[1].value[1].name]"; expected = 'three' } @{ text = "[parameters('test').index]"; expected = '1' } @{ text = "[parameters('test').objectArray[parameters('test').index].name]"; expected = 'two' } + @{ text = "[parameters('test')['hello']"; expected = '@{world=there}' } + @{ text = "[parameters('test')['hello']['world']]"; expected = 'there' } + @{ text = "[parameters('test')['array'][1][0]]"; expected = 'two' } + @{ text = "[parameters('test')['objectArray'][0]['name']]"; expected = 'one' } + @{ text = "[parameters('test')['objectArray'][1]['value'][1]['name']]"; expected = 'three' } ) { param($text, $expected) $yaml = @" @@ -185,7 +190,7 @@ resources: if ($out.results[0].result.actualState.output.$key -is [psobject]) { $out.results[0].result.actualState.output.$key.psobject.properties.value | Should -Be $expected.$key.values -Because ($out | ConvertTo-Json -Depth 10 | Out-String) } else { - $out.results[0].result.actualState.output.$key | Should -Be $expected.$key -Because ($out | ConvertTo-Json -Depth 10 | Out-String) + $out.results[0].result.actualState.output.$key | Should -Be $expected.$key -Because ($out | ConvertTo-Json -Depth 10 | Out-String) } } } @@ -240,7 +245,7 @@ resources: $LASTEXITCODE | Should -Be 2 $log = Get-Content -Path $TestDrive/error.log -Raw $log | Should -BeLike "*ERROR* Arguments must be of the same type*" - + } Context 'Resource name expression evaluation' { diff --git a/grammars/tree-sitter-dscexpression/grammar.js b/grammars/tree-sitter-dscexpression/grammar.js index d0f61e8f6..23ddadb59 100644 --- a/grammars/tree-sitter-dscexpression/grammar.js +++ b/grammars/tree-sitter-dscexpression/grammar.js @@ -42,7 +42,8 @@ module.exports = grammar({ memberAccess: $ => seq('.', field('name', $.memberName)), memberName: $ => /[a-zA-Z0-9_-]+/, - index: $ => seq('[', field('indexValue', choice($.expression, $.number)), ']'), + propertyName: $ => seq('\'', field('string', $.string), '\''), + index: $ => seq('[', field('indexValue', choice($.expression, $.number, $.propertyName)), ']'), } }); diff --git a/grammars/tree-sitter-dscexpression/test/corpus/invalid_expressions.txt b/grammars/tree-sitter-dscexpression/test/corpus/invalid_expressions.txt index 7defac4de..f97d03721 100644 --- a/grammars/tree-sitter-dscexpression/test/corpus/invalid_expressions.txt +++ b/grammars/tree-sitter-dscexpression/test/corpus/invalid_expressions.txt @@ -206,3 +206,12 @@ String with un-escaped single-quote (arguments (string)) (ERROR)))) + +===== +Object index without quotes +===== +[myObject[foo]] +--- + + (ERROR + (functionName)) diff --git a/grammars/tree-sitter-dscexpression/test/corpus/valid_expressions.txt b/grammars/tree-sitter-dscexpression/test/corpus/valid_expressions.txt index cb005a73d..b1163eba1 100644 --- a/grammars/tree-sitter-dscexpression/test/corpus/valid_expressions.txt +++ b/grammars/tree-sitter-dscexpression/test/corpus/valid_expressions.txt @@ -160,6 +160,24 @@ Array index (index (number))))) +===== +Object index +===== +[createObject('a',1)['a']] +--- + +(statement + (expression + (function + (functionName) + (arguments + (string) + (number))) + (accessor + (index + (propertyName + (string)))))) + ===== Multiple array indexes ===== diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 8f2982187..8577a0ef6 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -551,14 +551,18 @@ unavailableInUserFunction = "The 'variables()' function is not available in user [parser.expression] functionNodeNotFound = "Function node not found" -parsingFunction = "Parsing function '%{name}'" -parsingAccessor = "Parsing accessor '%{name}'" +parsingFunction = "Parsing function: %{name}" +parsingAccessor = "Parsing accessor: %{name}" accessorParsingError = "Error parsing accessor" -parsingMemberAccessor = "Parsing member accessor '%{name}'" +parsingMemberAccessor = "Parsing member accessor: %{name}" memberNotFound = "Member name not found" -parsingIndexAccessor = "Parsing index accessor '%{index}'" +parsingIndexAccessor = "Parsing index accessor: %{index}" indexNotFound = "Index value not found" -invalidAccessorKind = "Invalid accessor kind: '%{kind}'" +indexValue = "Index value: %{value} with kind %{kind}" +propertyNameValue = "Property name value: %{value}" +invalidPropertyName = "Invalid property name" +invalidIndexValueKind = "Invalid index value kind: %{kind}" +invalidAccessorKind = "Invalid accessor kind: %{kind}" functionResult = "Function results: %{results}" functionResultSecure = "Function result is secure" evalAccessors = "Evaluating accessors" diff --git a/lib/dsc-lib/src/parser/expressions.rs b/lib/dsc-lib/src/parser/expressions.rs index 16879126a..1383075f4 100644 --- a/lib/dsc-lib/src/parser/expressions.rs +++ b/lib/dsc-lib/src/parser/expressions.rs @@ -25,6 +25,11 @@ pub struct Expression { accessors: Vec, } +fn node_to_string(node: &Node, statement_bytes: &[u8]) -> Result { + let text = node.utf8_text(statement_bytes)?; + Ok(text.trim_matches('\'').to_string()) +} + impl Expression { /// Create a new `Expression` instance. /// @@ -41,11 +46,11 @@ impl Expression { let Some(function) = expression.child_by_field_name("function") else { return Err(DscError::Parser(t!("parser.expression.functionNodeNotFound").to_string())); }; - debug!("{}", t!("parser.expression.parsingFunction", name = function : {:?})); + debug!("{}", t!("parser.expression.parsingFunction", name = node_to_string(&function, statement_bytes)? : {:?})); let function = Function::new(statement_bytes, &function)?; let mut accessors = Vec::::new(); if let Some(accessor) = expression.child_by_field_name("accessor") { - debug!("{}", t!("parser.expression.parsingAccessor", name = accessor : {:?})); + debug!("{}", t!("parser.expression.parsingAccessor", name = node_to_string(&accessor, statement_bytes)? : {:?})); if accessor.is_error() { return Err(DscError::Parser(t!("parser.expression.accessorParsingError").to_string())); } @@ -57,7 +62,7 @@ impl Expression { let accessor_kind = accessor.kind(); let value = match accessor_kind { "memberAccess" => { - debug!("{}", t!("parser.expression.parsingMemberAccessor", name = accessor : {:?})); + debug!("{}", t!("parser.expression.parsingMemberAccessor", name = node_to_string(&accessor, statement_bytes)? : {:?})); let Some(member_name) = accessor.child_by_field_name("name") else { return Err(DscError::Parser(t!("parser.expression.memberNotFound").to_string())); }; @@ -65,22 +70,31 @@ impl Expression { Accessor::Member(member.to_string()) }, "index" => { - debug!("{}", t!("parser.expression.parsingIndexAccessor", index = accessor : {:?})); + debug!("{}", t!("parser.expression.parsingIndexAccessor", index = node_to_string(&accessor, statement_bytes)? : {:?})); let Some(index_value) = accessor.child_by_field_name("indexValue") else { return Err(DscError::Parser(t!("parser.expression.indexNotFound").to_string())); }; + debug!("{}", t!("parser.expression.indexValue", value = node_to_string(&index_value, statement_bytes)? : {:?}, kind = index_value.kind())); match index_value.kind() { "number" => { let value = index_value.utf8_text(statement_bytes)?; - let value = serde_json::from_str(value)?; - Accessor::Index(value) + let number: i64 = value.parse().map_err(|_| DscError::Parser(t!("parser.expression.indexNotValid").to_string()))?; + Accessor::Index(Value::Number(number.into())) + }, + "propertyName" => { + let Some(string_node) = index_value.child_by_field_name("string") else { + return Err(DscError::Parser(t!("parser.expression.propertyNameNotFound").to_string())); + }; + let value = string_node.utf8_text(statement_bytes)?; + debug!("{}", t!("parser.expression.propertyNameValue", value = value : {:?})); + Accessor::Index(Value::String(value.to_string())) }, "expression" => { let expression = Expression::new(statement_bytes, &index_value)?; Accessor::IndexExpression(expression) }, _ => { - return Err(DscError::Parser(t!("parser.expression.invalidAccessorKind", kind = accessor_kind).to_string())); + return Err(DscError::Parser(t!("parser.expression.invalidIndexValueKind", kind = index_value.kind()).to_string())); }, } }, @@ -186,6 +200,21 @@ impl Expression { return Err(DscError::Parser(t!("parser.expression.indexOnNonArray").to_string())); } } + else if index.is_string() { + let index = index.as_str().ok_or_else(|| DscError::Parser(t!("parser.expression.indexNotValid").to_string()))?; + if let Some(object) = value.as_object() { + if !object.contains_key(index) { + return Err(DscError::Parser(t!("parser.expression.memberNameNotFound", member = index).to_string())); + } + if is_secure { + value = convert_to_secure(&object[index]); + } else { + value = object[index].clone(); + } + } else { + return Err(DscError::Parser(t!("parser.expression.accessOnNonObject").to_string())); + } + } else if !index.is_null() { return Err(DscError::Parser(t!("parser.expression.invalidIndexType").to_string())); } From cd97cea2a4d4c76953ed47cf76a864bb87d593d6 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 13 Oct 2025 16:20:53 -0700 Subject: [PATCH 2/4] add object property index using function --- dsc/tests/dsc_expressions.tests.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dsc/tests/dsc_expressions.tests.ps1 b/dsc/tests/dsc_expressions.tests.ps1 index a537c0fdd..6c79fdfec 100644 --- a/dsc/tests/dsc_expressions.tests.ps1 +++ b/dsc/tests/dsc_expressions.tests.ps1 @@ -17,6 +17,7 @@ Describe 'Expressions tests' { @{ text = "[parameters('test')['array'][1][0]]"; expected = 'two' } @{ text = "[parameters('test')['objectArray'][0]['name']]"; expected = 'one' } @{ text = "[parameters('test')['objectArray'][1]['value'][1]['name']]"; expected = 'three' } + @{ text = "[parameters('test')[parameters('propertyName')]]"; expected = '@{world=there}' } ) { param($text, $expected) $yaml = @" @@ -40,6 +41,9 @@ Describe 'Expressions tests' { - nestedObject: name: three value: 3 + propertyName: + type: string + defaultValue: hello resources: - name: echo type: Microsoft.DSC.Debug/Echo From 2b3875da50250676d388aef93d46f108bba16b8f Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 13 Oct 2025 16:30:50 -0700 Subject: [PATCH 3/4] address copilot feedback --- lib/dsc-lib/locales/en-us.toml | 1 - lib/dsc-lib/src/parser/expressions.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 8577a0ef6..dc5619e8a 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -560,7 +560,6 @@ parsingIndexAccessor = "Parsing index accessor: %{index}" indexNotFound = "Index value not found" indexValue = "Index value: %{value} with kind %{kind}" propertyNameValue = "Property name value: %{value}" -invalidPropertyName = "Invalid property name" invalidIndexValueKind = "Invalid index value kind: %{kind}" invalidAccessorKind = "Invalid accessor kind: %{kind}" functionResult = "Function results: %{results}" diff --git a/lib/dsc-lib/src/parser/expressions.rs b/lib/dsc-lib/src/parser/expressions.rs index 1383075f4..5cbeeeb55 100644 --- a/lib/dsc-lib/src/parser/expressions.rs +++ b/lib/dsc-lib/src/parser/expressions.rs @@ -27,7 +27,7 @@ pub struct Expression { fn node_to_string(node: &Node, statement_bytes: &[u8]) -> Result { let text = node.utf8_text(statement_bytes)?; - Ok(text.trim_matches('\'').to_string()) + Ok(text.to_string()) } impl Expression { From e5226f169746527239dccb30cce3c58ae0be6889 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 13 Oct 2025 16:59:50 -0700 Subject: [PATCH 4/4] fix missing text --- lib/dsc-lib/locales/en-us.toml | 1 + lib/dsc-lib/src/parser/expressions.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index dc5619e8a..3e813e818 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -572,6 +572,7 @@ indexNotValid = "Index is not a valid number" indexOutOfBounds = "Index is out of bounds" indexOnNonArray = "Index access on non-array value" invalidIndexType = "Invalid index type" +propertyNameNotString = "Property name is not a string" [parser.functions] foundErrorNode = "Found error node parsing function" diff --git a/lib/dsc-lib/src/parser/expressions.rs b/lib/dsc-lib/src/parser/expressions.rs index 5cbeeeb55..ea568b38e 100644 --- a/lib/dsc-lib/src/parser/expressions.rs +++ b/lib/dsc-lib/src/parser/expressions.rs @@ -83,7 +83,7 @@ impl Expression { }, "propertyName" => { let Some(string_node) = index_value.child_by_field_name("string") else { - return Err(DscError::Parser(t!("parser.expression.propertyNameNotFound").to_string())); + return Err(DscError::Parser(t!("parser.expression.propertyNameNotString").to_string())); }; let value = string_node.utf8_text(statement_bytes)?; debug!("{}", t!("parser.expression.propertyNameValue", value = value : {:?}));