Skip to content

Commit 45789c5

Browse files
authored
Merge pull request #514 from SteveL-MSFT/array-index
Add array index support to expression grammar
2 parents 47479f7 + accbd26 commit 45789c5

File tree

12 files changed

+1725
-320
lines changed

12 files changed

+1725
-320
lines changed

dsc/src/subcommand.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pub fn config_get(configurator: &mut Configurator, format: &Option<OutputFormat>
4949
}
5050
},
5151
Err(err) => {
52-
error!("Error: {err}");
52+
error!("{err}");
5353
exit(EXIT_DSC_ERROR);
5454
}
5555
}

dsc/tests/dsc_expressions.tests.ps1

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'Expressions tests' {
5+
It 'Accessors work: <text>' -TestCases @(
6+
@{ text = "[parameters('test').hello]"; expected = '@{world=there}' }
7+
@{ text = "[parameters('test').hello.world]"; expected = 'there' }
8+
@{ text = "[parameters('test').array[0]]"; expected = 'one' }
9+
@{ text = "[parameters('test').array[1][1]]"; expected = 'three' }
10+
@{ text = "[parameters('test').objectArray[0].name]"; expected = 'one' }
11+
@{ text = "[parameters('test').objectArray[1].value[0]]"; expected = '2' }
12+
@{ text = "[parameters('test').objectArray[1].value[1].name]"; expected = 'three' }
13+
) {
14+
param($text, $expected)
15+
$yaml = @"
16+
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
17+
parameters:
18+
test:
19+
type: object
20+
defaultValue:
21+
hello:
22+
world: there
23+
array:
24+
- one
25+
- [ 'two', 'three' ]
26+
objectArray:
27+
- name: one
28+
value: 1
29+
- name: two
30+
value:
31+
- 2
32+
- nestedObject:
33+
name: three
34+
value: 3
35+
resources:
36+
- name: echo
37+
type: Test/Echo
38+
properties:
39+
output: "$text"
40+
"@
41+
$debug = $yaml | dsc -l debug config get -f yaml 2>&1 | Out-String
42+
$out = $yaml | dsc config get | ConvertFrom-Json
43+
$LASTEXITCODE | Should -Be 0
44+
$out.results[0].result.actualState.output | Should -Be $expected -Because $debug
45+
}
46+
47+
It 'Invalid expressions: <expression>' -TestCases @(
48+
@{ expression = "[concat('A','B')].hello" }
49+
@{ expression = "[concat('A','B')](0)" }
50+
@{ expression = "[concat('a','b').hello]" }
51+
@{ expression = "[concat('a','b')[0]]" }
52+
) {
53+
param($expression)
54+
$yaml = @"
55+
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json
56+
resources:
57+
- name: echo
58+
type: Test/Echo
59+
properties:
60+
output: "$expression"
61+
"@
62+
$out = dsc config get -d $yaml 2>&1
63+
$LASTEXITCODE | Should -Be 2
64+
$out | Should -BeLike "*ERROR*"
65+
}
66+
}

dsc_lib/src/parser/expressions.rs

Lines changed: 93 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@ use crate::dscerror::DscError;
1010
use crate::functions::FunctionDispatcher;
1111
use crate::parser::functions::Function;
1212

13+
#[derive(Clone)]
14+
pub enum Accessor {
15+
Member(String),
16+
Index(Value),
17+
}
18+
1319
#[derive(Clone)]
1420
pub struct Expression {
1521
function: Function,
16-
member_access: Option<Vec<String>>,
22+
accessors: Vec<Accessor>,
1723
}
1824

1925
impl Expression {
@@ -32,64 +38,115 @@ impl Expression {
3238
let Some(function) = expression.child_by_field_name("function") else {
3339
return Err(DscError::Parser("Function node not found".to_string()));
3440
};
41+
debug!("Parsing function '{:?}'", function);
3542
let function = Function::new(statement_bytes, &function)?;
36-
let member_access = if let Some(members) = expression.child_by_field_name("members") {
37-
if members.is_error() {
38-
return Err(DscError::Parser("Error parsing dot-notation".to_string()));
43+
let mut accessors = Vec::<Accessor>::new();
44+
if let Some(accessor) = expression.child_by_field_name("accessor") {
45+
debug!("Parsing accessor '{:?}'", accessor);
46+
if accessor.is_error() {
47+
return Err(DscError::Parser("Error parsing accessor".to_string()));
3948
}
40-
let mut result = vec![];
41-
let mut cursor = members.walk();
42-
for member in members.named_children(&mut cursor) {
43-
if member.is_error() {
44-
return Err(DscError::Parser("Error parsing dot-notation member".to_string()));
49+
let mut cursor = accessor.walk();
50+
for accessor in accessor.named_children(&mut cursor) {
51+
if accessor.is_error() {
52+
return Err(DscError::Parser("Error parsing accessor".to_string()));
4553
}
46-
let value = member.utf8_text(statement_bytes)?;
47-
result.push(value.to_string());
54+
let accessor_kind = accessor.kind();
55+
let value = match accessor_kind {
56+
"memberAccess" => {
57+
debug!("Parsing member accessor '{:?}'", accessor);
58+
let Some(member_name) = accessor.child_by_field_name("name") else {
59+
return Err(DscError::Parser("Member name not found".to_string()));
60+
};
61+
let member = member_name.utf8_text(statement_bytes)?;
62+
Accessor::Member(member.to_string())
63+
},
64+
"index" => {
65+
debug!("Parsing index accessor '{:?}'", accessor);
66+
let Some(index_value) = accessor.child_by_field_name("indexValue") else {
67+
return Err(DscError::Parser("Index value not found".to_string()));
68+
};
69+
match index_value.kind() {
70+
"number" => {
71+
let value = index_value.utf8_text(statement_bytes)?;
72+
let value = serde_json::from_str(value)?;
73+
Accessor::Index(value)
74+
},
75+
"expression" => {
76+
return Err(DscError::Parser("Expression index not supported".to_string()));
77+
},
78+
_ => {
79+
return Err(DscError::Parser(format!("Invalid accessor kind: '{accessor_kind}'")));
80+
},
81+
}
82+
},
83+
_ => {
84+
return Err(DscError::Parser(format!("Invalid accessor kind: '{accessor_kind}'")));
85+
},
86+
};
87+
accessors.push(value);
4888
}
49-
Some(result)
5089
}
51-
else {
52-
None
53-
};
90+
5491
Ok(Expression {
5592
function,
56-
member_access,
93+
accessors,
5794
})
5895
}
5996

6097
/// Invoke the expression.
6198
///
99+
/// # Arguments
100+
///
101+
/// * `function_dispatcher` - The function dispatcher to use.
102+
/// * `context` - The context to use.
103+
///
104+
/// # Returns
105+
///
106+
/// The result of the expression.
107+
///
62108
/// # Errors
63109
///
64110
/// This function will return an error if the expression fails to execute.
65111
pub fn invoke(&self, function_dispatcher: &FunctionDispatcher, context: &Context) -> Result<Value, DscError> {
66112
let result = self.function.invoke(function_dispatcher, context)?;
67113
trace!("Function result: '{:?}'", result);
68-
if let Some(member_access) = &self.member_access {
69-
debug!("Evaluating member access '{:?}'", member_access);
70-
if !result.is_object() {
71-
return Err(DscError::Parser("Member access on non-object value".to_string()));
72-
}
73-
114+
if self.accessors.is_empty() {
115+
Ok(result)
116+
}
117+
else {
118+
debug!("Evaluating accessors");
74119
let mut value = result;
75-
for member in member_access {
76-
if !value.is_object() {
77-
return Err(DscError::Parser(format!("Member access '{member}' on non-object value")));
78-
}
79-
80-
if let Some(object) = value.as_object() {
81-
if !object.contains_key(member) {
82-
return Err(DscError::Parser(format!("Member '{member}' not found")));
83-
}
84-
85-
value = object[member].clone();
120+
for accessor in &self.accessors {
121+
match accessor {
122+
Accessor::Member(member) => {
123+
if let Some(object) = value.as_object() {
124+
if !object.contains_key(member) {
125+
return Err(DscError::Parser(format!("Member '{member}' not found")));
126+
}
127+
value = object[member].clone();
128+
} else {
129+
return Err(DscError::Parser("Member access on non-object value".to_string()));
130+
}
131+
},
132+
Accessor::Index(index) => {
133+
if let Some(array) = value.as_array() {
134+
let Some(index) = index.as_u64() else {
135+
return Err(DscError::Parser("Index is not a valid number".to_string()));
136+
};
137+
let index = usize::try_from(index)?;
138+
if index >= array.len() {
139+
return Err(DscError::Parser("Index out of bounds".to_string()));
140+
}
141+
value = array[index].clone();
142+
} else {
143+
return Err(DscError::Parser("Index access on non-array value".to_string()));
144+
}
145+
},
86146
}
87147
}
88148

89149
Ok(value)
90150
}
91-
else {
92-
Ok(result)
93-
}
94151
}
95152
}

dsc_lib/src/parser/mod.rs

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -52,43 +52,45 @@ impl Statement {
5252
if root_node.is_error() {
5353
return Err(DscError::Parser(format!("Error parsing statement root: {statement}")));
5454
}
55-
let root_node_kind = root_node.kind();
56-
if root_node_kind != "statement" {
55+
if root_node.kind() != "statement" {
5756
return Err(DscError::Parser(format!("Invalid statement: {statement}")));
5857
}
59-
let Some(child_node) = root_node.named_child(0) else {
60-
return Err(DscError::Parser("Child node not found".to_string()));
61-
};
62-
if child_node.is_error() {
63-
return Err(DscError::Parser("Error parsing statement child".to_string()));
64-
}
65-
let kind = child_node.kind();
6658
let statement_bytes = statement.as_bytes();
67-
match kind {
68-
"stringLiteral" | "bracketInStringLiteral" => {
69-
let Ok(value) = child_node.utf8_text(statement_bytes) else {
70-
return Err(DscError::Parser("Error parsing string literal".to_string()));
71-
};
72-
debug!("Parsing string literal: {0}", value.to_string());
73-
Ok(Value::String(value.to_string()))
74-
},
75-
"escapedStringLiteral" => {
76-
// need to remove the first character: [[ => [
77-
let Ok(value) = child_node.utf8_text(statement_bytes) else {
78-
return Err(DscError::Parser("Error parsing escaped string literal".to_string()));
79-
};
80-
debug!("Parsing escaped string literal: {0}", value[1..].to_string());
81-
Ok(Value::String(value[1..].to_string()))
82-
},
83-
"expression" => {
84-
debug!("Parsing expression");
85-
let expression = Expression::new(statement_bytes, &child_node)?;
86-
Ok(expression.invoke(&self.function_dispatcher, context)?)
87-
},
88-
_ => {
89-
Err(DscError::Parser(format!("Unknown expression type {0}", child_node.kind())))
59+
let mut cursor = root_node.walk();
60+
let mut return_value = Value::Null;
61+
for child_node in root_node.named_children(&mut cursor) {
62+
if child_node.is_error() {
63+
return Err(DscError::Parser(format!("Error parsing statement: {statement}")));
64+
}
65+
66+
match child_node.kind() {
67+
"stringLiteral" => {
68+
let Ok(value) = child_node.utf8_text(statement_bytes) else {
69+
return Err(DscError::Parser("Error parsing string literal".to_string()));
70+
};
71+
debug!("Parsing string literal: {0}", value.to_string());
72+
return_value = Value::String(value.to_string());
73+
},
74+
"escapedStringLiteral" => {
75+
// need to remove the first character: [[ => [
76+
let Ok(value) = child_node.utf8_text(statement_bytes) else {
77+
return Err(DscError::Parser("Error parsing escaped string literal".to_string()));
78+
};
79+
debug!("Parsing escaped string literal: {0}", value[1..].to_string());
80+
return_value = Value::String(value[1..].to_string());
81+
},
82+
"expression" => {
83+
debug!("Parsing expression");
84+
let expression = Expression::new(statement_bytes, &child_node)?;
85+
return_value = expression.invoke(&self.function_dispatcher, context)?;
86+
},
87+
_ => {
88+
return Err(DscError::Parser(format!("Unknown expression type {0}", child_node.kind())));
89+
}
9090
}
9191
}
92+
93+
Ok(return_value)
9294
}
9395
}
9496

@@ -113,8 +115,8 @@ mod tests {
113115
#[test]
114116
fn bracket_in_string() {
115117
let mut parser = Statement::new().unwrap();
116-
let result = parser.parse_and_execute("[this] is a string", &Context::new()).unwrap();
117-
assert_eq!(result, "[this] is a string");
118+
let result = parser.parse_and_execute("[this] is a string", &Context::new());
119+
assert!(result.is_err());
118120
}
119121

120122
#[test]

tree-sitter-dscexpression/.npmrc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
; We generally want to save install/update commands
2+
save=true
3+
; We use a public Azure Artifacts mirror
14
registry=https://pkgs.dev.azure.com/powershell/PowerShell/_packaging/powershell/npm/registry/
2-
35
always-auth=true
6+
; But we don't want references to it in the lockfile
7+
omit-lockfile-registry-resolved=true

tree-sitter-dscexpression/build.ps1

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT License.
33

4-
# check if tools are installed
4+
param(
5+
[switch]$UpdatePackages
6+
)
57

68
function Invoke-NativeCommand($cmd) {
79
Invoke-Expression $cmd
@@ -10,6 +12,8 @@ function Invoke-NativeCommand($cmd) {
1012
}
1113
}
1214

15+
$env:TREE_SITTER_VERBOSE=1
16+
1317
if ($null -eq (Get-Command npm -ErrorAction Ignore)) {
1418
Write-Host 'Installing Node'
1519

@@ -30,5 +34,18 @@ if ($LASTEXITCODE -ne 0) {
3034
npm ci tree-sitter-cli --omit=optional
3135
}
3236

33-
Invoke-NativeCommand 'npx tree-sitter generate'
37+
if ($UpdatePackages) {
38+
if (!$IsWindows) {
39+
throw "This switch only works on Windows"
40+
}
41+
42+
rm ./package-lock.json
43+
rm -r ./node_modules
44+
npm cache clean --force
45+
npm logout
46+
vsts-npm-auth -config .npmrc -F -V
47+
npm install --force --verbose
48+
}
49+
50+
Invoke-NativeCommand 'npx tree-sitter generate --build'
3451
Invoke-NativeCommand 'npx tree-sitter test'

0 commit comments

Comments
 (0)