Skip to content

Commit b4b1c41

Browse files
Add loadDirectoryFileInfo function (#17241)
## Description Fixes #3607 Add `loadDirectoryFileInfo` function to retrieve basic information about a directory's content ## Checklist - [x] I have read and adhere to the [contribution guide](https://github.com/Azure/bicep/blob/main/CONTRIBUTING.md). ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/Azure/bicep/pull/17241) --------- Co-authored-by: Anthony Martin <[email protected]>
1 parent 567ca0a commit b4b1c41

File tree

68 files changed

+2259
-293
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+2259
-293
lines changed

src/Bicep.Core.IntegrationTests/Scenarios/LoadFunctionsTests.cs

Lines changed: 188 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
3131
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
3232
";
3333
private static readonly string B64_TEXT_CONTENT = Convert.ToBase64String(Encoding.UTF8.GetBytes(TEXT_CONTENT));
34-
public enum FunctionCase { loadTextContent, loadFileAsBase64, loadJsonContent, loadYamlContent }
34+
public enum FunctionCase { loadTextContent, loadFileAsBase64, loadJsonContent, loadYamlContent, loadDirectoryFileInfo }
3535
private static string ExpectedResult(FunctionCase function) => function switch
3636
{
3737
FunctionCase.loadTextContent => TEXT_CONTENT,
@@ -291,6 +291,13 @@ public void LoadFunction_RequiresCompileTimeConstantArguments_Valid(FunctionCase
291291
encoding: encoding
292292
}
293293
]", "files[0].name", "'$'", "files[0].encoding", DisplayName = "loadYamlContent: encoding param as object property in array")]
294+
[DataRow(FunctionCase.loadDirectoryFileInfo, @"param searchPattern string = '*')
295+
var directories = [
296+
{
297+
path: './'
298+
searchPattern: searchPattern
299+
}
300+
]", "directories[0].path", "directories[0].searchPattern", DisplayName = "loadDirectoryFileInfo: searchPattern param as object property in array")]
294301
public void LoadFunction_RequiresCompileTimeConstantArguments_Invalid(FunctionCase function, string declaration, params string[] args)
295302
{
296303
//notice - here we will not test actual loading file with given encoding - just the fact that bicep function accepts all .NET available encodings
@@ -1043,5 +1050,185 @@ public void LoadYamlContent_DisallowsUnknownEncoding(string encoding)
10431050
}
10441051
}
10451052

1053+
private readonly string TEST_FILES_ARM = """
1054+
[
1055+
{
1056+
"relativePath": "File.json",
1057+
"baseName": "File.json",
1058+
"extension": ".json"
1059+
},
1060+
{
1061+
"relativePath": "main.bicep",
1062+
"baseName": "main.bicep",
1063+
"extension": ".bicep",
1064+
}
1065+
]
1066+
""";
1067+
1068+
1069+
// Users are likely to use "*" instead of "" as a wildcard so we test that "" and "*" behave similarly
1070+
[DataRow(true)]
1071+
[DataRow(false)]
1072+
[DataTestMethod]
1073+
public void LoadDirectoryFileInfoFunction(bool withWildCard)
1074+
{
1075+
var (template, diags, _) = CompilationHelper.Compile(
1076+
("main.bicep", $"var fileObjs = loadDirectoryFileInfo('./'{(withWildCard ? ", '*'" : "")})"),
1077+
("File.json", ""));
1078+
1079+
1080+
using (new AssertionScope())
1081+
{
1082+
template!.Should().NotBeNull();
1083+
diags.ExcludingLinterDiagnostics().Should().BeEmpty();
1084+
}
1085+
using (new AssertionScope())
1086+
{
1087+
template!.SelectToken("$.variables.fileObjs").Should().DeepEqual("[variables('$fxv#0')]");
1088+
var expectedContent = TEST_FILES_ARM;
1089+
template!.SelectToken("$.variables['$fxv#0']").Should().DeepEqual(JToken.Parse(expectedContent));
1090+
}
1091+
}
1092+
1093+
[DataRow("*.json", "main.bicep")]
1094+
[DataRow("File*", "main.bicep")]
1095+
[DataRow("Fi*.js*", "main.bicep")]
1096+
[DataRow("*e.js*", "main.bicep")]
1097+
[DataRow("File?json", "main.bicep")]
1098+
[DataRow("*.bicep", "File.json")]
1099+
[DataRow("main*", "File.json")]
1100+
[DataRow("ma*.bi*", "File.json")]
1101+
[DataRow("*n.bi*", "File.json")]
1102+
[DataRow("main?bicep", "File.json")]
1103+
[DataTestMethod]
1104+
public void LoadDirectoryFileInfoWithPattern(string searchPattern, string fileToExclude)
1105+
{
1106+
var fullContent = TEST_FILES_ARM;
1107+
var loadedContent = JToken.Parse(fullContent);
1108+
loadedContent.Should().NotBeNull();
1109+
var tokenToRemove = loadedContent!.FirstOrDefault(t => t.Value<string>("baseName") == fileToExclude);
1110+
tokenToRemove.Should().NotBeNull();
1111+
tokenToRemove!.Parent.Should().NotBeNull();
1112+
tokenToRemove!.Remove();
1113+
var (template, diags, _) = CompilationHelper.Compile(
1114+
("main.bicep", $"var fileObjs = loadDirectoryFileInfo('./', '{searchPattern}')"),
1115+
("File.json", ""));
1116+
1117+
using (new AssertionScope())
1118+
{
1119+
template!.Should().NotBeNull();
1120+
diags.ExcludingLinterDiagnostics().Should().BeEmpty();
1121+
}
1122+
using (new AssertionScope())
1123+
{
1124+
template!.SelectToken("$.variables.fileObjs").Should().DeepEqual("[variables('$fxv#0')]");
1125+
template!.SelectToken("$.variables['$fxv#0']").Should().DeepEqual(loadedContent);
1126+
}
1127+
}
1128+
1129+
[TestMethod]
1130+
public void LoadDirectoryFileInfoShouldReturnNothingWhenDirIsEmpty()
1131+
{
1132+
var (template, diags, _) = CompilationHelper.Compile(
1133+
("main.bicep", $"var fileObjs = loadDirectoryFileInfo('../../')"),
1134+
("File.json", ""));
1135+
1136+
using (new AssertionScope())
1137+
{
1138+
template!.Should().NotBeNull();
1139+
diags.ExcludingLinterDiagnostics().Should().BeEmpty();
1140+
}
1141+
using (new AssertionScope())
1142+
{
1143+
template!.SelectToken("$.variables.fileObjs").Should().DeepEqual("[variables('$fxv#0')]");
1144+
template!.SelectToken("$.variables['$fxv#0']").Should().DeepEqual(JToken.Parse("[]"));
1145+
}
1146+
}
1147+
1148+
1149+
[TestMethod]
1150+
public void LoadDirectoryFileInfoErrorWhenFileDoesNotExist()
1151+
{
1152+
var directoryPath = "./nonExistingDirectory";
1153+
var (template, diags, _) = CompilationHelper.Compile(
1154+
("main.bicep", $"var fileObjs = loadDirectoryFileInfo('{directoryPath}')"),
1155+
("File.json", ""));
1156+
1157+
using (new AssertionScope())
1158+
{
1159+
template!.Should().BeNull();
1160+
diags.ExcludingLinterDiagnostics().Should().HaveDiagnostics([
1161+
("BCP428", DiagnosticLevel.Error, $"Directory \"{directoryPath}\" does not exist or additional permissions are necessary to access it.")
1162+
]);
1163+
}
1164+
}
1165+
1166+
[TestMethod]
1167+
public void LoadDirectoryFileInfo_returns_error_if_file_path_used_instead_of_dir()
1168+
{
1169+
var directoryPath = "./File.json";
1170+
var (template, diags, _) = CompilationHelper.Compile(
1171+
("main.bicep", $"var fileObjs = loadDirectoryFileInfo('{directoryPath}')"),
1172+
("File.json", ""));
1173+
1174+
using (new AssertionScope())
1175+
{
1176+
template.Should().BeNull();
1177+
diags.ExcludingLinterDiagnostics().Should().HaveDiagnostics([
1178+
("BCP430", DiagnosticLevel.Error, $"Unable to open directory at path \"{directoryPath}\". Found a file instead.")
1179+
]);
1180+
}
1181+
}
1182+
1183+
[DataRow("/")]
1184+
[DataRow("/helloWorld")]
1185+
[DataRow("/path/to")]
1186+
[DataTestMethod]
1187+
public void LoadDirectoryFileInfoErrorWhenRootedPath(string rootedPath)
1188+
{
1189+
var (template, diags, _) = CompilationHelper.Compile(
1190+
("main.bicep", $"var fileObjs = loadDirectoryFileInfo('{rootedPath}')"),
1191+
("File.json", ""));
1192+
1193+
using (new AssertionScope())
1194+
{
1195+
template!.Should().BeNull();
1196+
diags.ExcludingLinterDiagnostics().Should().HaveDiagnostics(new[] { ("BCP051", DiagnosticLevel.Error, "The specified path begins with \"/\". Files must be referenced using relative paths.") });
1197+
}
1198+
}
1199+
1200+
[DataRow("C:/")]
1201+
[DataRow("C:/helloworld")]
1202+
[DataRow("C:/path/to")]
1203+
[DataTestMethod]
1204+
public void LoadDirectoryFileInfoErrorWhenRootedPathWindows(string rootedPath)
1205+
{
1206+
var (template, diags, _) = CompilationHelper.Compile(
1207+
("main.bicep", $"var fileObjs = loadDirectoryFileInfo('{rootedPath}')"),
1208+
("File.json", ""));
1209+
1210+
using (new AssertionScope())
1211+
{
1212+
template!.Should().BeNull();
1213+
diags.ExcludingLinterDiagnostics().Should().HaveDiagnostics(new[] { ("BCP085", DiagnosticLevel.Error, "The specified file path contains one ore more invalid path characters. The following are not permitted: \"\"\", \"*\", \":\", \"<\", \">\", \"?\", \"\\\", \"|\".") });
1214+
}
1215+
}
1216+
1217+
[DataRow(" ")]
1218+
[DataRow(".")]
1219+
[DataTestMethod]
1220+
public void LoadDirectoryFileInfoErrorWhenPathIsDotOrEmpty(string path)
1221+
{
1222+
var (template, diags, _) = CompilationHelper.Compile(
1223+
("main.bicep", $"var fileObjs = loadDirectoryFileInfo('{path}')"),
1224+
("File.json", ""));
1225+
1226+
using (new AssertionScope())
1227+
{
1228+
template!.Should().BeNull();
1229+
diags.ExcludingLinterDiagnostics().Should().HaveDiagnostics(new[] { ("BCP086", DiagnosticLevel.Error, "The specified file path ends with an invalid character. The following are not permitted: \" \", \".\".") });
1230+
}
1231+
}
1232+
10461233
}
10471234
}

src/Bicep.Core.Samples/Files/baselines/Functions/sys.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,32 @@
931931
"arg: array"
932932
]
933933
},
934+
{
935+
"name": "loadDirectoryFileInfo",
936+
"description": "Loads basic information about a directory's files as bicep object. File loading occurs during compilation, not at runtime.",
937+
"fixedParameters": [
938+
{
939+
"name": "directoryPath",
940+
"description": "The path to the directory that will be loaded.",
941+
"type": "string",
942+
"required": true
943+
},
944+
{
945+
"name": "searchPattern",
946+
"description": "The searchPattern is a glob pattern to narrow down the loaded files. If not provided, all files are loaded. Supports both any number of characters '*' and any single character '?' wildcards.",
947+
"type": "string",
948+
"required": false
949+
}
950+
],
951+
"minimumArgumentCount": 1,
952+
"maximumArgumentCount": 2,
953+
"flags": "generateIntermediateVariableAlways",
954+
"typeSignature": "(directoryPath: string, [searchPattern: string]): any",
955+
"parameterTypeSignatures": [
956+
"directoryPath: string",
957+
"[searchPattern: string]"
958+
]
959+
},
934960
{
935961
"name": "loadFileAsBase64",
936962
"description": "Loads the specified file as base64 string. File loading occurs during compilation, not at runtime. The maximum allowed size is 96 Kb.",

src/Bicep.Core.Samples/Files/baselines/InvalidExpressions_LF/Completions/symbols.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1553,6 +1553,27 @@
15531553
"command": "editor.action.triggerParameterHints"
15541554
}
15551555
},
1556+
{
1557+
"label": "loadDirectoryFileInfo",
1558+
"kind": "function",
1559+
"documentation": {
1560+
"kind": "markdown",
1561+
"value": "```bicep\nloadDirectoryFileInfo(directoryPath: string, [searchPattern: string]): any\n\n``` \nLoads basic information about a directory's files as bicep object. File loading occurs during compilation, not at runtime. \n"
1562+
},
1563+
"deprecated": false,
1564+
"preselect": false,
1565+
"sortText": "3_loadDirectoryFileInfo",
1566+
"insertTextFormat": "snippet",
1567+
"insertTextMode": "adjustIndentation",
1568+
"textEdit": {
1569+
"range": {},
1570+
"newText": "loadDirectoryFileInfo($0)"
1571+
},
1572+
"command": {
1573+
"title": "signature help",
1574+
"command": "editor.action.triggerParameterHints"
1575+
}
1576+
},
15561577
{
15571578
"label": "loadFileAsBase64",
15581579
"kind": "function",

src/Bicep.Core.Samples/Files/baselines/InvalidExpressions_LF/Completions/sysFunctions.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,27 @@
776776
"command": "editor.action.triggerParameterHints"
777777
}
778778
},
779+
{
780+
"label": "loadDirectoryFileInfo",
781+
"kind": "function",
782+
"documentation": {
783+
"kind": "markdown",
784+
"value": "```bicep\nloadDirectoryFileInfo(directoryPath: string, [searchPattern: string]): any\n\n``` \nLoads basic information about a directory's files as bicep object. File loading occurs during compilation, not at runtime. \n"
785+
},
786+
"deprecated": false,
787+
"preselect": false,
788+
"sortText": "3_loadDirectoryFileInfo",
789+
"insertTextFormat": "snippet",
790+
"insertTextMode": "adjustIndentation",
791+
"textEdit": {
792+
"range": {},
793+
"newText": "loadDirectoryFileInfo($0)"
794+
},
795+
"command": {
796+
"title": "signature help",
797+
"command": "editor.action.triggerParameterHints"
798+
}
799+
},
779800
{
780801
"label": "loadFileAsBase64",
781802
"kind": "function",

src/Bicep.Core.Samples/Files/baselines/InvalidModules_LF/Completions/symbolsPlusX.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1450,6 +1450,27 @@
14501450
"command": "editor.action.triggerParameterHints"
14511451
}
14521452
},
1453+
{
1454+
"label": "loadDirectoryFileInfo",
1455+
"kind": "function",
1456+
"documentation": {
1457+
"kind": "markdown",
1458+
"value": "```bicep\nloadDirectoryFileInfo(directoryPath: string, [searchPattern: string]): any\n\n``` \nLoads basic information about a directory's files as bicep object. File loading occurs during compilation, not at runtime. \n"
1459+
},
1460+
"deprecated": false,
1461+
"preselect": false,
1462+
"sortText": "3_loadDirectoryFileInfo",
1463+
"insertTextFormat": "snippet",
1464+
"insertTextMode": "adjustIndentation",
1465+
"textEdit": {
1466+
"range": {},
1467+
"newText": "loadDirectoryFileInfo($0)"
1468+
},
1469+
"command": {
1470+
"title": "signature help",
1471+
"command": "editor.action.triggerParameterHints"
1472+
}
1473+
},
14531474
{
14541475
"label": "loadFileAsBase64",
14551476
"kind": "function",

src/Bicep.Core.Samples/Files/baselines/InvalidModules_LF/Completions/symbolsPlusX_if.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1450,6 +1450,27 @@
14501450
"command": "editor.action.triggerParameterHints"
14511451
}
14521452
},
1453+
{
1454+
"label": "loadDirectoryFileInfo",
1455+
"kind": "function",
1456+
"documentation": {
1457+
"kind": "markdown",
1458+
"value": "```bicep\nloadDirectoryFileInfo(directoryPath: string, [searchPattern: string]): any\n\n``` \nLoads basic information about a directory's files as bicep object. File loading occurs during compilation, not at runtime. \n"
1459+
},
1460+
"deprecated": false,
1461+
"preselect": false,
1462+
"sortText": "3_loadDirectoryFileInfo",
1463+
"insertTextFormat": "snippet",
1464+
"insertTextMode": "adjustIndentation",
1465+
"textEdit": {
1466+
"range": {},
1467+
"newText": "loadDirectoryFileInfo($0)"
1468+
},
1469+
"command": {
1470+
"title": "signature help",
1471+
"command": "editor.action.triggerParameterHints"
1472+
}
1473+
},
14531474
{
14541475
"label": "loadFileAsBase64",
14551476
"kind": "function",

src/Bicep.Core.Samples/Files/baselines/InvalidOutputs_CRLF/Completions/arrayPlusSymbols.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,27 @@
898898
"command": "editor.action.triggerParameterHints"
899899
}
900900
},
901+
{
902+
"label": "loadDirectoryFileInfo",
903+
"kind": "function",
904+
"documentation": {
905+
"kind": "markdown",
906+
"value": "```bicep\nloadDirectoryFileInfo(directoryPath: string, [searchPattern: string]): any\n\n``` \nLoads basic information about a directory's files as bicep object. File loading occurs during compilation, not at runtime. \n"
907+
},
908+
"deprecated": false,
909+
"preselect": false,
910+
"sortText": "3_loadDirectoryFileInfo",
911+
"insertTextFormat": "snippet",
912+
"insertTextMode": "adjustIndentation",
913+
"textEdit": {
914+
"range": {},
915+
"newText": "loadDirectoryFileInfo($0)"
916+
},
917+
"command": {
918+
"title": "signature help",
919+
"command": "editor.action.triggerParameterHints"
920+
}
921+
},
901922
{
902923
"label": "loadFileAsBase64",
903924
"kind": "function",

0 commit comments

Comments
 (0)