diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..2bc35cb5 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,94 @@ +# Copilot Instructions for PowerApps-Tooling + +## Build and Test + +This is a .NET SDK project (SDK 10.0, pinned in `global.json`). The solution is `src/PASopa.sln`. + +```shell +# Build +dotnet build src/PASopa.sln + +# Run all tests (Windows only — includes net48 targets) +dotnet test --no-build --solution src/PASopa.sln + +# Run a specific test project +dotnet test --no-build --project src/Persistence.Tests/Persistence.Tests.csproj +dotnet test --no-build --project src/PAModelTests/PAModelTests.csproj + +# Run a single test by name +dotnet test --no-build --project src/Persistence.Tests/Persistence.Tests.csproj --filter "FullyQualifiedName~MsappArchiveTests.SomeTestName" + +# Run tests for a specific target framework +dotnet test --no-build --project src/Persistence.Tests/Persistence.Tests.csproj --framework net10.0 + +# Clean repo (removes all build artifacts except src/.vs) +scorch.cmd +``` + +Multi-targeting: Projects target `net8.0`, `net10.0`, and `net48`. Building `net48` requires Windows with .NET Framework 4.8 Developer Pack. On Linux/macOS, `net48` targets are skipped automatically. + +Warnings are treated as errors at the command line (`TreatWarningsAsErrors` is set in `Directory.Build.props`), but suppressed in VS IDE to avoid blocking development flow. + +## Architecture + +### Two library generations + +- **Persistence** (`src/Persistence/`): The active library (`Microsoft.PowerPlatform.PowerApps.Persistence`). Handles the modern `.msapp` format with YAML-based source representation (`.pa.yaml` files). This is where new work happens. The 'Source Code' schema of canvas YAML (aka PaYamlV3). +- **PAModel** (`src/PAModel/`): Legacy library (`Microsoft.PowerPlatform.Formulas.Tools`) for the older PASopa pack/unpack tool. No longer actively developed, although some maintenance may still occur. The 'Experimental' schema of canvas YAML (aka PaYamlV1). + +### Persistence library structure + +- **`Compression/`** — Cross-platform archive abstraction (`PaArchive`, `PaArchiveEntry`, `PaArchivePath`) with both sync and async extract APIs. +- **`MsApp/`** — Archive model: `MsappArchive` wraps `ZipArchive` to read/write `.msapp` files. `MsappLayoutConstants` centralizes all archive entry paths. Factory pattern via `IMsappArchiveFactory`. +- **`MsappPacking/`** — Core pack/unpack orchestration: `MsappPackingService` converts between `.msapp` archives and unpacked source directory structures. Unpack validates version constraints (`MinSupportedMSAppStructureVersion`, `MinSupportedDocVersion`) before extracting. Pack reads `.msapr` reference files and `Src/**/*.pa.yaml` to rebuild archives. +- **`PaYaml/`** — YAML serialization layer: `PaYamlSerializer` is the central entry point, built on YamlDotNet. Uses strongly-typed models like `NamedObject` for YAML document structure. The models in `src\Persistence\PaYaml\Models\SchemaV3\**` represent the 'Source Code' schema of canvas YAML (PaYamlV3). These models should match the schema defined in `src/schemas/pa-yaml/v3.0/` and are used for both serialization and deserialization. +- **`TfmAdapters/` and `TfmExtensions/`** — Polyfills and adapter shims so modern C# features work on `net48` (using PolySharp). +- **`Extensions/`** — Helper extension methods for JSON, LINQ, and strings. + +### Dependency injection + +Services are registered via `IServiceCollection` extension methods using `TryAddSingleton`: +- `services.AddMsappArchiveFactory()` — registers `IMsappArchiveFactory` +- `services.AddMsappPackingService()` — registers `MsappPackingService` and its dependencies + +### Error handling + +`PersistenceLibraryException` with categorized `PersistenceErrorCode` (1xxx = System, 2xxx = Serialization, 3xxx = Deserialization, 4xxx = Archive). Exceptions carry optional context: `MsappEntryFullPath`, `LineNumber`, `Column`, `JsonPath`. + +### Schemas + +JSON schemas for the `.pa.yaml` format live in `src/schemas/pa-yaml/` (versioned: `v3.0`). Published schemas go to the root `schemas/` directory via `src/schemas/publish.ps1`. + +## `YamlValidator` project + +This project only worked with PaYamlV2, which has been deprecated. It's only here for historical reference and as a starter in case we want to repurpose it to validate PaYamlV3. + +## Conventions + +### Testing + +- Test framework is **MSTest** via MSTest.Sdk (configured in `global.json`). +- Test classes inherit from `TestBase` (which extends `VSTestBase`) for shared helpers like `JsonShouldBeEquivalentTo()`, `ToJsonElement()`, and test output folder management. +- Serialization tests inherit from `SerializationTestBase`. +- Assertions use **FluentAssertions** (pinned ≤7.x due to license — see `Directory.Packages.props`). +- Test data lives in `_TestData/` directories within test projects, organized by scenario (`ValidYaml/`, `InvalidYaml/`, `AppsWithYaml/`, etc.). +- `Persistence.Testing` project provides shared test utilities (`CapturingLogger`, `FilePathComparer`). + +### Package version constraints + +Several packages have upper version limits due to license changes — these are documented with `WARNING` comments in `src/Directory.Packages.props`: +- FluentAssertions: ≤7.x +- JsonSchema.Net: ≤8.0.5 +- Yaml2JsonNode: ≤2.4.0 + +### Code style + +- C# language version: 13.0 (set in Persistence csproj) +- Nullable reference types: enabled +- File-scoped namespaces (e.g., `namespace Foo;`) +- Primary constructors used for DI (e.g., `MsappPackingService(IMsappArchiveFactory, ...)`) +- Root namespace: `Microsoft.PowerPlatform.PowerApps.Persistence` for Persistence, `Microsoft.PowerPlatform.Formulas.Tools` for PAModel +- Copyright header: `// Copyright (c) Microsoft Corporation.` + `// Licensed under the MIT License.` +- Central package management via `src/Directory.Packages.props` +- Global usings for `System`, `System.Collections.Generic`, `System.Diagnostics` defined in `src/Directory.Build.props` +- InternalsVisibleTo for test assemblies is configured with signing key in `Directory.Build.targets` diff --git a/src/PAModel/.editorconfig b/src/PAModel/.editorconfig index c4297cbe..8939a81c 100644 --- a/src/PAModel/.editorconfig +++ b/src/PAModel/.editorconfig @@ -1,13 +1,6 @@ [*.cs] -# Category overries: -# Disabling these categories because the original implementation didn't have them on and we're not making changes to it at this time. -dotnet_analyzer_diagnostic.category-Design.severity = silent -dotnet_analyzer_diagnostic.category-Globalization.severity = silent -dotnet_analyzer_diagnostic.category-Performance.severity = silent -dotnet_analyzer_diagnostic.category-Usage.severity = silent - # Rule overrides: -# CA1716: Identifiers should not match keywords -dotnet_diagnostic.CA1716.severity = silent +# IDE0130: Namespace does not match folder structure +dotnet_diagnostic.IDE0130.severity = silent diff --git a/src/PAModel/CanvasDocument.cs b/src/PAModel/CanvasDocument.cs index e22a799b..ee1372f4 100644 --- a/src/PAModel/CanvasDocument.cs +++ b/src/PAModel/CanvasDocument.cs @@ -33,7 +33,7 @@ public class CanvasDocument // Track all unknown "files". Ensures round-tripping isn't lossy. // Only contains files of FileKind.Unknown - internal Dictionary _unknownFiles = new(); + internal Dictionary _unknownFiles = []; // Key is Top Parent Control Name for both _screens and _components internal Dictionary _screens = new(StringComparer.Ordinal); @@ -47,7 +47,7 @@ public class CanvasDocument // Also includes entries for DataSources made from a DataComponent // Key is parent entity name (datasource name for non cds data sources) internal Dictionary> _dataSources = new(StringComparer.Ordinal); - internal List _screenOrder = new(); + internal List _screenOrder = []; internal HeaderJson _header; internal DocumentPropertiesJson _properties; @@ -88,7 +88,7 @@ public class CanvasDocument internal ChecksumJson _checksum; // Track all asset files, key is file name - internal Dictionary _assetFiles = new(); + internal Dictionary _assetFiles = []; internal UniqueIdRestorer _idRestorer; @@ -96,7 +96,7 @@ public class CanvasDocument // This dictionary stores the metadata information for that file - like OriginalName, NewFileName, Path... // Key is a (case-insensitive) new fileName of the resource. // Reason for using FileName of the resource as the key is to avoid name collision across different types eg. Images/close.png, Videos/close.mp4. - internal Dictionary _localAssetInfoJson = new(); + internal Dictionary _localAssetInfoJson = []; internal static string AssetFilePathPrefix = @"Assets\"; #region Save/Load @@ -123,11 +123,9 @@ public static (CanvasDocument, ErrorContainer) LoadFromMsapp(string fullPathToMs return (null, errors); } - using (var stream = new FileStream(fullPathToMsApp, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - var doc = Wrapper(() => MsAppSerializer.Load(stream, errors), errors); - return (doc, errors); - } + using var stream = new FileStream(fullPathToMsApp, FileMode.Open, FileAccess.Read, FileShare.Read); + var doc = Wrapper(() => MsAppSerializer.Load(stream, errors), errors); + return (doc, errors); } public static (CanvasDocument, ErrorContainer) LoadFromMsapp(Stream streamToMsapp) @@ -287,7 +285,7 @@ internal CanvasDocument(CanvasDocument other) _templateStore = new TemplateStore(other._templateStore); _dataSources = other._dataSources.JsonClone(); - _screenOrder = new List(other._screenOrder); + _screenOrder = [.. other._screenOrder]; _header = other._header.JsonClone(); _properties = other._properties.JsonClone(); @@ -323,7 +321,7 @@ internal void AddDataSourceForLoad(DataSourceEntry ds, int? order = null) var key = ds.RelatedEntityName ?? ds.Name; if (!_dataSources.TryGetValue(key, out var list)) { - list = new List(); + list = []; _dataSources.Add(key, list); } @@ -486,14 +484,17 @@ internal void OnLoadComplete(ErrorContainer errors) } // Integrity checks. - foreach (var kv in _connections.NullOk()) + if (_connections is not null) { - var connection = kv.Value; - - if (kv.Key != connection.id) + foreach (var kv in _connections) { - errors.FormatNotSupported($"Document consistency error. Connection id mismatch"); - throw new DocumentException(); + var connection = kv.Value; + + if (kv.Key != connection.id) + { + errors.FormatNotSupported($"Document consistency error. Connection id mismatch"); + throw new DocumentException(); + } } } } @@ -514,7 +515,7 @@ internal HashSet GetImportedComponents() private static FilePath GetAssetFilePathWithoutPrefix(string path) { - return FilePath.FromMsAppPath(path.Substring(AssetFilePathPrefix.Length)); + return FilePath.FromMsAppPath(path[AssetFilePathPrefix.Length..]); } internal void StabilizeAssetFilePaths(ErrorContainer errors) @@ -676,16 +677,11 @@ private void RestoreAssetFilePaths(ErrorContainer errors) } // Helper for traversing and ensuring unique control names. - internal class UniqueControlNameVisitor + internal class UniqueControlNameVisitor(ErrorContainer errors) { + private readonly ErrorContainer _errors = errors; // Control names are case sensitive. private readonly Dictionary _names = new(StringComparer.Ordinal); - private readonly ErrorContainer _errors; - - public UniqueControlNameVisitor(ErrorContainer errors) - { - _errors = errors; - } public void Visit(BlockNode node) { diff --git a/src/PAModel/Checksum/ChecksumMaker.cs b/src/PAModel/Checksum/ChecksumMaker.cs index c702fe9e..a85560e2 100644 --- a/src/PAModel/Checksum/ChecksumMaker.cs +++ b/src/PAModel/Checksum/ChecksumMaker.cs @@ -25,7 +25,7 @@ public class ChecksumMaker public const string ChecksumName = "checksum.json"; // Track a checksum per file and then merge into a single one at the end. - private readonly Dictionary _files = new(); + private readonly Dictionary _files = []; public static (string wholeChecksum, Dictionary perFileChecksum) GetChecksum(string fullpathToMsApp) { @@ -80,8 +80,8 @@ internal static byte[] ChecksumFile(string filename, byte[] bytes) } // These paths are json double-encoded and need a different comparer. - private static readonly HashSet _jsonDouble = new() - { + private static readonly HashSet _jsonDouble = + [ "LocalConnectionReferences", "LocalDatabaseReferences", "LibraryDependencies", @@ -95,21 +95,21 @@ internal static byte[] ChecksumFile(string filename, byte[] bytes) "Template\\DynamicControlDefinitionJson\\Properties", "Template\\DynamicControlDefinitionJson\\Resources", "Template\\DynamicControlDefinitionJson\\SubscribedFunctionalities", - }; + ]; // These paths are xml double-encoded and need a different comparer. - private static readonly HashSet _xmlDouble = new() - { + private static readonly HashSet _xmlDouble = + [ "UsedTemplates\\Template", "DataSources\\WadlMetadata\\WadlXml", - }; + ]; // Helper for identifying which paths are double encoded. // All of these should be resolved and fixed by the server. private class Context { public string Filename; - public Stack s = new(); + public Stack s = []; public void Push(string path) { @@ -187,7 +187,7 @@ internal static string ChecksumToString(byte[] bytes) internal static int GetChecksumVersion(string checksum) { var version = checksum.Split('_').First(); - return int.Parse(version.Substring(1)); + return int.Parse(version[1..]); } /// diff --git a/src/PAModel/Checksum/IHashMaker.cs b/src/PAModel/Checksum/IHashMaker.cs index 6d4c4d78..aeead931 100644 --- a/src/PAModel/Checksum/IHashMaker.cs +++ b/src/PAModel/Checksum/IHashMaker.cs @@ -37,11 +37,11 @@ internal class Sha256HashMaker : IHashMaker, IDisposable private static readonly byte[] _endObj = "}"u8.ToArray(); private static readonly byte[] _startArray = "["u8.ToArray(); private static readonly byte[] _endArray = "]"u8.ToArray(); - private static readonly byte[] _null = new byte[] { 254 }; - private static readonly byte[] _true = new byte[] { 1 }; - private static readonly byte[] _false = new byte[] { 0 }; + private static readonly byte[] _null = [254]; + private static readonly byte[] _true = [1]; + private static readonly byte[] _false = [0]; - private static readonly byte[] _marker = new byte[] { 255 }; + private static readonly byte[] _marker = [255]; public Sha256HashMaker() { diff --git a/src/PAModel/ControlTemplates/CommonControlProperties.cs b/src/PAModel/ControlTemplates/CommonControlProperties.cs index 71302e03..f8da2784 100644 --- a/src/PAModel/ControlTemplates/CommonControlProperties.cs +++ b/src/PAModel/ControlTemplates/CommonControlProperties.cs @@ -10,7 +10,7 @@ namespace Microsoft.PowerPlatform.Formulas.Tools.ControlTemplates; internal class CommonControlProperties { // Key is property name - private readonly Dictionary _properties = new(); + private readonly Dictionary _properties = []; private const string FileName = "Microsoft.PowerPlatform.Formulas.Tools.ControlTemplates.commonStyleProperties.xml"; private static CommonControlProperties _instance; diff --git a/src/PAModel/ControlTemplates/ControlTemplate.cs b/src/PAModel/ControlTemplates/ControlTemplate.cs index 6802b05b..8b4f3687 100644 --- a/src/PAModel/ControlTemplates/ControlTemplate.cs +++ b/src/PAModel/ControlTemplates/ControlTemplate.cs @@ -20,7 +20,7 @@ public ControlTemplate(string name, string version, string id) Name = name; Version = version; Id = id; - InputDefaults = new Dictionary(); - VariantDefaultValues = new Dictionary>(); + InputDefaults = []; + VariantDefaultValues = []; } } diff --git a/src/PAModel/ControlTemplates/DynamicProperties.cs b/src/PAModel/ControlTemplates/DynamicProperties.cs index 86a546ed..7567bee2 100644 --- a/src/PAModel/ControlTemplates/DynamicProperties.cs +++ b/src/PAModel/ControlTemplates/DynamicProperties.cs @@ -21,8 +21,8 @@ internal static bool AddsChildDynamicProperties(string template, string variant) } - private static readonly HashSet _supportsNestedControls = new() - { + private static readonly HashSet _supportsNestedControls = + [ "dataCard", "dataGrid", "dataTable", @@ -38,7 +38,7 @@ internal static bool AddsChildDynamicProperties(string template, string variant) "layoutContainer", "pcfDataField", "typedDataCard", - }; + ]; private static readonly IReadOnlyDictionary> PropertyDefaultScriptGetters = new Dictionary>() { diff --git a/src/PAModel/EditorState/TemplateStore.cs b/src/PAModel/EditorState/TemplateStore.cs index 57af4b89..c7ed8cb4 100644 --- a/src/PAModel/EditorState/TemplateStore.cs +++ b/src/PAModel/EditorState/TemplateStore.cs @@ -12,7 +12,7 @@ internal class TemplateStore public TemplateStore() { - Contents = new Dictionary(); + Contents = []; } public TemplateStore(TemplateStore other) diff --git a/src/PAModel/Entropy.cs b/src/PAModel/Entropy.cs index 1e33e6e6..b789e7ab 100644 --- a/src/PAModel/Entropy.cs +++ b/src/PAModel/Entropy.cs @@ -95,7 +95,7 @@ internal class PropertyEntropy public HashSet AppTestsMissingStepsMetadata { get; set; } = new HashSet(StringComparer.Ordinal); // Key is connection id, value is connection instance id - public Dictionary LocalConnectionIDReferences { get; set; } = new Dictionary(); + public Dictionary LocalConnectionIDReferences { get; set; } = []; // Key is test rule, value is test screen id without Screen name public Dictionary RuleScreenIdWithoutScreen { get; set; } = new Dictionary(StringComparer.Ordinal); @@ -192,8 +192,8 @@ public static string GetResourcesJsonIndicesKey(ResourceJson resource) // Removing the 'ContentKind-' gives the resource name public static string GetResourceNameFromKey(string key) { - var prefix = key.Split(new char[] { '-' }).First(); - return key.Substring(prefix.Length + 1); + var prefix = key.Split(['-']).First(); + return key[(prefix.Length + 1)..]; } public void SetHeaderLastSaved(DateTime? x) @@ -274,7 +274,7 @@ public int GetGroupControlOrder(string groupName, string childName) public void AddDataTableControlJson(string controlName, string json) { - DataTableCustomControlTemplateJsons ??= new Dictionary(); + DataTableCustomControlTemplateJsons ??= []; DataTableCustomControlTemplateJsons.Add(controlName, json); } diff --git a/src/PAModel/Extensions/CollectionsExtensions.cs b/src/PAModel/Extensions/CollectionsExtensions.cs index 4b8d34d6..05883d71 100644 --- a/src/PAModel/Extensions/CollectionsExtensions.cs +++ b/src/PAModel/Extensions/CollectionsExtensions.cs @@ -8,18 +8,6 @@ namespace Microsoft.PowerPlatform.Formulas.Tools.Extensions; public static class CollectionsExtensions { - // Allows using with { } initializers, which require an Add() method. - public static void Add(this Stack stack, T item) - { - stack.Push(item); - } - - public static IEnumerable NullOk(this IEnumerable list) - { - if (list == null) return Enumerable.Empty(); - return list; - } - public static void AddRange( this IDictionary thisDictionary, IEnumerable> other) @@ -55,9 +43,7 @@ public static TValue GetOrCreate(this IDictionary di public static IList Clone(this IList obj) where T : ICloneable { - if (obj == null) - return null; - return obj.Select(item => item.Clone()).ToList(); + return obj?.Select(item => item.Clone()).ToList(); } } diff --git a/src/PAModel/Extensions/JsonExtensions.cs b/src/PAModel/Extensions/JsonExtensions.cs index 86aef0ba..2632b233 100644 --- a/src/PAModel/Extensions/JsonExtensions.cs +++ b/src/PAModel/Extensions/JsonExtensions.cs @@ -29,35 +29,35 @@ private static JsonSerializerOptions GetJsonOptions() return opts; } - public static JsonSerializerOptions _jsonOpts = GetJsonOptions(); + internal static JsonSerializerOptions JsonOpts = GetJsonOptions(); // https://stackoverflow.com/questions/58138793/system-text-json-jsonelement-toobject-workaround public static string JsonSerialize(T obj) { - return JsonSerializer.Serialize(obj, _jsonOpts); + return JsonSerializer.Serialize(obj, JsonOpts); } public static T JsonParse(string json) { - return JsonSerializer.Deserialize(json, _jsonOpts); + return JsonSerializer.Deserialize(json, JsonOpts); } public static T ToObject(this JsonElement element) { var json = element.GetRawText(); - return JsonSerializer.Deserialize(json, _jsonOpts); + return JsonSerializer.Deserialize(json, JsonOpts); } public static T ToObject(this JsonDocument document) { var json = document.RootElement.GetRawText(); - return JsonSerializer.Deserialize(json, _jsonOpts); + return JsonSerializer.Deserialize(json, JsonOpts); } public static byte[] ToBytes(this JsonElement e) { - var bytes = JsonSerializer.SerializeToUtf8Bytes(e, _jsonOpts); + var bytes = JsonSerializer.SerializeToUtf8Bytes(e, JsonOpts); return bytes; } @@ -65,8 +65,8 @@ public static byte[] ToBytes(this JsonElement e) // but don't want to mutate the original. public static T JsonClone(this T obj) { - var str = JsonSerializer.Serialize(obj, _jsonOpts); - var obj2 = JsonSerializer.Deserialize(str, _jsonOpts); + var str = JsonSerializer.Serialize(obj, JsonOpts); + var obj2 = JsonSerializer.Deserialize(str, JsonOpts); return obj2; } diff --git a/src/PAModel/Extensions/StringExtensions.cs b/src/PAModel/Extensions/StringExtensions.cs index 27cb9554..7b31c981 100644 --- a/src/PAModel/Extensions/StringExtensions.cs +++ b/src/PAModel/Extensions/StringExtensions.cs @@ -9,7 +9,7 @@ public static class StringExtensions { public static string UnEscapePAString(this string text) { - return text.Substring(1, text.Length - 2).Replace("\"\"", "\""); + return text[1..^1].Replace("\"\"", "\""); } public static string EscapePAString(this string text) @@ -19,11 +19,8 @@ public static string EscapePAString(this string text) public static string FirstCharToUpper(this string input) { - return input switch - { - null => throw new ArgumentNullException(nameof(input)), - "" => throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input)), - _ => input.First().ToString().ToUpper() + input.Substring(1) - }; + ThrowIfNullOrEmpty(input); + + return $"{char.ToUpper(input[0])}{input[1..]}"; } } diff --git a/src/PAModel/IO/ControlPath.cs b/src/PAModel/IO/ControlPath.cs index 0975588a..ad71c096 100644 --- a/src/PAModel/IO/ControlPath.cs +++ b/src/PAModel/IO/ControlPath.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Immutable; using System.Linq; namespace Microsoft.PowerPlatform.Formulas.Tools.IO; @@ -10,17 +11,19 @@ namespace Microsoft.PowerPlatform.Formulas.Tools.IO; /// Each segment is a control name /// [DebuggerDisplay("{string.Join('.', _segments)}")] -internal class ControlPath +internal class ControlPath(IEnumerable segments) { + public static ControlPath Empty => new([]); + // switch this to be a queue? - private readonly List _segments; - public string Current => _segments.Any() ? _segments[0] : null; - public static ControlPath Empty => new(new List()); + private readonly ImmutableArray _segments = [.. segments]; + + public string Current => _segments.Length != 0 ? _segments[0] : null; public ControlPath Next() { var newSegments = new List(); - for (var i = 1; i < _segments.Count; ++i) + for (var i = 1; i < _segments.Length; ++i) { newSegments.Add(_segments[i]); } @@ -36,11 +39,6 @@ public ControlPath Append(string controlName) return new ControlPath(newPath); } - public ControlPath(List segments) - { - _segments = segments; - } - public static bool operator ==(ControlPath left, ControlPath right) { return left?.Equals(right) ?? right is null; @@ -54,7 +52,7 @@ public ControlPath(List segments) public override bool Equals(object obj) { return obj is ControlPath other && - other._segments.Count == _segments.Count && + other._segments.Length == _segments.Length && _segments.SequenceEqual(other._segments); } diff --git a/src/PAModel/IO/DirectoryReader.cs b/src/PAModel/IO/DirectoryReader.cs index b73947d7..ae2b2e72 100644 --- a/src/PAModel/IO/DirectoryReader.cs +++ b/src/PAModel/IO/DirectoryReader.cs @@ -64,7 +64,7 @@ public T ToObject() else { var str = File.ReadAllText(_fullpath); - return JsonSerializer.Deserialize(str, JsonExtensions._jsonOpts); + return JsonSerializer.Deserialize(str, JsonExtensions.JsonOpts); } } @@ -81,7 +81,7 @@ public Entry[] EnumerateFiles(string subdir, string pattern = "*", bool searchSu if (!Directory.Exists(root)) { - return new Entry[0]; + return []; } var fullPaths = Directory.EnumerateFiles(root, pattern, searchSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); @@ -104,7 +104,7 @@ public DirectoryReader[] EnumerateDirectories(string subdir, string pattern = "* if (!Directory.Exists(root)) { - return new DirectoryReader[0]; + return []; } var fullPaths = Directory.EnumerateDirectories(root, pattern, searchSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); diff --git a/src/PAModel/IO/DirectoryWriter.cs b/src/PAModel/IO/DirectoryWriter.cs index 2fe0388e..6f2b2640 100644 --- a/src/PAModel/IO/DirectoryWriter.cs +++ b/src/PAModel/IO/DirectoryWriter.cs @@ -68,7 +68,7 @@ public void WriteAllJson(string subdir, FilePath filename, T obj) } else { - var text = JsonSerializer.Serialize(obj, JsonExtensions._jsonOpts); + var text = JsonSerializer.Serialize(obj, JsonExtensions.JsonOpts); text = JsonNormalizer.Normalize(text); WriteAllText(subdir, filename, text); } @@ -77,7 +77,7 @@ public void WriteAllJson(string subdir, FilePath filename, T obj) // Use this if the filename is already escaped. public void WriteAllJson(string subdir, string filename, T obj) { - var text = JsonSerializer.Serialize(obj, JsonExtensions._jsonOpts); + var text = JsonSerializer.Serialize(obj, JsonExtensions.JsonOpts); text = JsonNormalizer.Normalize(text); WriteAllText(subdir, filename, text); } diff --git a/src/PAModel/IO/FilePath.cs b/src/PAModel/IO/FilePath.cs index 5554286a..1afe9784 100644 --- a/src/PAModel/IO/FilePath.cs +++ b/src/PAModel/IO/FilePath.cs @@ -15,11 +15,17 @@ public class FilePath public const int MaxNameLength = 50; private const string yamlExtension = ".fx.yaml"; private const string editorStateExtension = ".editorstate.json"; + private readonly string[] _pathSegments; public FilePath(params string[] segments) { - _pathSegments = segments ?? (new string[] { }); + _pathSegments = segments ?? []; + } + + public FilePath(IEnumerable segments) + { + _pathSegments = segments?.ToArray() ?? []; } public static bool IsYamlFile(FilePath path) @@ -52,7 +58,7 @@ public string ToMsAppPath() var path = string.Join("\\", _pathSegments); // Some paths mistakenly start with DirectorySepChar in the msapp, - // We replaced it with `_/` when writing, remove that now. + // We replaced it with `_/` when writing, remove that now. if (path.StartsWith(FileEntry.FilenameLeadingUnderscore.ToString())) { path = path.TrimStart(FileEntry.FilenameLeadingUnderscore); @@ -75,19 +81,14 @@ public static void EnsurePathRooted(string path) /// public static string GetResourceRelativePath(ContentKind contentType) { - switch (contentType) + return contentType switch { - case ContentKind.Image: - return @"Assets\Images"; - case ContentKind.Audio: - return @"Assets\Audio"; - case ContentKind.Video: - return @"Assets\Video"; - case ContentKind.Pdf: - return @"Assets\Pdf"; - default: - throw new NotSupportedException("Unrecognized Content Kind for local resource"); - } + ContentKind.Image => @"Assets\Images", + ContentKind.Audio => @"Assets\Audio", + ContentKind.Video => @"Assets\Video", + ContentKind.Pdf => @"Assets\Pdf", + _ => throw new NotSupportedException("Unrecognized Content Kind for local resource"), + }; } /// @@ -103,7 +104,7 @@ public static string GetResourceRelativePath(ContentKind contentType) /// Thrown if or path is null or an empty string. /// /// Want to use Path.GetRelativePath() from Net 2.1. But since we target netstandard 2.0, we need to shim it. - /// Convert to URIs and make the relative path. + /// Convert to URIs and make the relative path. /// see https://stackoverflow.com/questions/275689/how-to-get-relative-path-from-absolute-path /// For reference, see Core's impl at: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs#L861 /// @@ -277,7 +278,7 @@ public static string TruncateNameIfTooLong(string name) private static string TruncateName(string name, int length) { var removeTrailingCharsLength = name[length - 1] == EscapeChar ? 1 : (name[length - 2] == EscapeChar ? 2 : 0); - return name.Substring(0, length - removeTrailingCharsLength); + return name[..(length - removeTrailingCharsLength)]; } /// @@ -299,7 +300,7 @@ public static ulong GetHash(string str) public string ToPlatformPath() { - return Path.Combine(_pathSegments.Select(EscapeFilename).ToArray()); + return Path.Combine([.. _pathSegments.Select(EscapeFilename)]); } public static FilePath FromPlatformPath(string path) @@ -307,7 +308,7 @@ public static FilePath FromPlatformPath(string path) if (path == null) return new FilePath(); var segments = path.Split(Path.DirectorySeparatorChar).Select(UnEscapeFilename); - return new FilePath(segments.ToArray()); + return new FilePath(segments); } public static FilePath FromMsAppPath(string path) @@ -323,21 +324,17 @@ public static FilePath ToFilePath(string path) if (path == null) return new FilePath(); var segments = path.Split(Path.DirectorySeparatorChar).Select(x => x); - return new FilePath(segments.ToArray()); + return new FilePath(segments); } public static FilePath RootedAt(string root, FilePath remainder) { - var segments = new List() { root }; - segments.AddRange(remainder._pathSegments); - return new FilePath(segments.ToArray()); + return new FilePath(remainder._pathSegments.Prepend(root)); } public FilePath Append(string segment) { - var newSegments = new List(_pathSegments); - newSegments.Add(segment); - return new FilePath(newSegments.ToArray()); + return new FilePath(_pathSegments.Append(segment)); } public bool StartsWith(string root, StringComparison stringComparison) @@ -402,8 +399,8 @@ public string HandleFileNameCollisions(string path) var suffixCounter = 0; var fileName = GetFileName(); var extension = GetCustomExtension(fileName); - var fileNameWithoutExtension = fileName.Substring(0, fileName.Length - extension.Length); - var pathWithoutFileName = path.Substring(0, path.Length - fileName.Length); + var fileNameWithoutExtension = fileName[..^extension.Length]; + var pathWithoutFileName = path[..^fileName.Length]; while (File.Exists(path)) { var filename = fileNameWithoutExtension + '_' + ++suffixCounter + extension; diff --git a/src/PAModel/IR/IRNode.cs b/src/PAModel/IR/IRNode.cs index 0a1b2f6e..8422f7b1 100644 --- a/src/PAModel/IR/IRNode.cs +++ b/src/PAModel/IR/IRNode.cs @@ -23,9 +23,9 @@ internal abstract class IRNode internal class BlockNode : IRNode, ICloneable, IEquatable { public TypedNameNode Name; - public IList Properties = new List(); - public IList Functions = new List(); - public IList Children = new List(); + public IList Properties = []; + public IList Functions = []; + public IList Children = []; public override void Accept(IRNodeVisitor visitor, Context context) { @@ -196,8 +196,8 @@ public override int GetHashCode() internal class FunctionNode : IRNode, ICloneable, IEquatable { public string Identifier; - public IList Args = new List(); - public IList Metadata = new List(); + public IList Args = []; + public IList Metadata = []; public override void Accept(IRNodeVisitor visitor, Context context) { diff --git a/src/PAModel/IR/IRStateHelpers.cs b/src/PAModel/IR/IRStateHelpers.cs index fde22114..3d35838d 100644 --- a/src/PAModel/IR/IRStateHelpers.cs +++ b/src/PAModel/IR/IRStateHelpers.cs @@ -50,8 +50,8 @@ private static void SplitIRAndState(Item control, string topParentName, int inde customProp.PropertyScopeKey.PropertyScopeRulesKey); // Skip component property params on instances - customPropsToHide = new HashSet(customPropScopeRules - .Select(propertyScopeRule => propertyScopeRule.Name)); + customPropsToHide = [.. customPropScopeRules + .Select(propertyScopeRule => propertyScopeRule.Name)]; foreach (var arg in customPropScopeRules) { @@ -61,7 +61,7 @@ private static void SplitIRAndState(Item control, string topParentName, int inde if (invariantScript != null && invariantScript != arg.ScopeVariableInfo.DefaultRule) { var argKey = $"{control.Name}.{arg.Name}"; - entropy.FunctionParamsInvariantScriptsOnInstances.Add(argKey, new string[] { arg.ScopeVariableInfo.DefaultRule, invariantScript }); + entropy.FunctionParamsInvariantScriptsOnInstances.Add(argKey, [arg.ScopeVariableInfo.DefaultRule, invariantScript]); } } } @@ -111,7 +111,7 @@ private static void SplitIRAndState(Item control, string topParentName, int inde if (invariantScript != null && invariantScript != arg.ScopeVariableInfo.DefaultRule) { var argKey = $"{control.Name}.{arg.Name}"; - entropy.FunctionParamsInvariantScripts.Add(argKey, new string[] { arg.ScopeVariableInfo.DefaultRule, invariantScript }); + entropy.FunctionParamsInvariantScripts.Add(argKey, [arg.ScopeVariableInfo.DefaultRule, invariantScript]); } arg.ScopeVariableInfo.DefaultRule = null; @@ -258,7 +258,7 @@ private static void SplitIRAndState(Item control, string topParentName, int inde Name = control.Name, TopParentName = topParentName, Properties = propStates, - DynamicProperties = dynPropStates.Any() ? dynPropStates : null, + DynamicProperties = dynPropStates.Count != 0 ? dynPropStates : null, HasDynamicProperties = control.HasDynamicProperties, StyleName = control.StyleName, IsGroupControl = control.IsGroupControl, @@ -433,7 +433,7 @@ private static (Item item, int index) CombineIRAndState(BlockNode blockNode, Err ControlUniqueId = uniqueId.ToString(), VariantName = variantName ?? string.Empty, Rules = properties.ToArray(), - DynamicProperties = (isInResponsiveLayout && dynamicProperties.Any()) ? dynamicProperties.ToArray() : null, + DynamicProperties = (isInResponsiveLayout && dynamicProperties.Count != 0) ? dynamicProperties.ToArray() : null, HasDynamicProperties = state.HasDynamicProperties, StyleName = state.StyleName, ExtensionData = state.ExtensionData, @@ -483,7 +483,7 @@ private static (Item item, int index) CombineIRAndState(BlockNode blockNode, Err } } resultControlInfo.Rules = properties.ToArray(); - var hasDynamicProperties = isInResponsiveLayout && dynamicProperties.Any(); + var hasDynamicProperties = isInResponsiveLayout && dynamicProperties.Count != 0; resultControlInfo.DynamicProperties = hasDynamicProperties ? dynamicProperties.ToArray() : null; resultControlInfo.HasDynamicProperties = hasDynamicProperties; resultControlInfo.AllowAccessToGlobals = templateState?.ComponentManifest?.AllowAccessToGlobals; diff --git a/src/PAModel/IR/UniqueIdRestorer.cs b/src/PAModel/IR/UniqueIdRestorer.cs index 8574a3d1..0fb444d2 100644 --- a/src/PAModel/IR/UniqueIdRestorer.cs +++ b/src/PAModel/IR/UniqueIdRestorer.cs @@ -15,7 +15,7 @@ public UniqueIdRestorer(Entropy entropy) { _controlUniqueIds = entropy.ControlUniqueIds; _controlUniqueGuids = entropy.ControlUniqueGuids; - _nextId = (_controlUniqueIds.Any() ? Math.Max(2, _controlUniqueIds.Values.Max()) : 2) + 1; + _nextId = (_controlUniqueIds.Count != 0 ? Math.Max(2, _controlUniqueIds.Values.Max()) : 2) + 1; } public string GetControlId(string controlName) diff --git a/src/PAModel/IsExternalInit.cs b/src/PAModel/IsExternalInit.cs deleted file mode 100644 index c17deffc..00000000 --- a/src/PAModel/IsExternalInit.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -#if NETFRAMEWORK || NETSTANDARD2_0 -#pragma warning disable - -using System.ComponentModel; - -namespace System.Runtime.CompilerServices; - -// C# 9 init property setters and record types do not quite function out-of-the-box -// for net4.8, but do so long as an "System.Runtime.CompilerServices.IsExternalInit" -// type merely exists. -[EditorBrowsable(EditorBrowsableState.Never)] -#if TFMADAPTERS_PUBLIC -public -#else -internal -#endif -static class IsExternalInit -{ -} - -#pragma warning restore -#endif diff --git a/src/PAModel/MergeTool/ControlDiffVisitor.cs b/src/PAModel/MergeTool/ControlDiffVisitor.cs index 0f33de0c..fe2fb40d 100644 --- a/src/PAModel/MergeTool/ControlDiffVisitor.cs +++ b/src/PAModel/MergeTool/ControlDiffVisitor.cs @@ -19,14 +19,14 @@ internal class ControlDiffVisitor : DefaultVisitor public static IEnumerable GetControlDelta(BlockNode ours, BlockNode parent, EditorStateStore childStateStore, TemplateStore parentTemplateStore, TemplateStore childTemplateStore, bool isInComponent) { var visitor = new ControlDiffVisitor(childStateStore, parentTemplateStore, childTemplateStore); - visitor.Visit(ours, new ControlDiffContext(new ControlPath(new List()), parent, isInComponent)); + visitor.Visit(ours, new ControlDiffContext(new ControlPath([]), parent, isInComponent)); return visitor._deltas; } private ControlDiffVisitor(EditorStateStore childStateStore, TemplateStore parentTemplateStore, TemplateStore childTemplateStore) { - _deltas = new List(); + _deltas = []; _childStateStore = childStateStore; _parentTemplateStore = parentTemplateStore; _childTemplateStore = childTemplateStore; diff --git a/src/PAModel/Microsoft.PowerPlatform.Formulas.Tools.csproj b/src/PAModel/Microsoft.PowerPlatform.Formulas.Tools.csproj index 0f0b6f40..2d32e4a3 100644 --- a/src/PAModel/Microsoft.PowerPlatform.Formulas.Tools.csproj +++ b/src/PAModel/Microsoft.PowerPlatform.Formulas.Tools.csproj @@ -1,7 +1,7 @@ - + net48;net8.0;net10.0 - 12 + 13 true true @@ -22,8 +22,8 @@ - - + + @@ -32,11 +32,21 @@ + + + + + + + + + + diff --git a/src/PAModel/PAConvert/ErrorContainer.cs b/src/PAModel/PAConvert/ErrorContainer.cs index 40c7fcd6..e4a1298d 100644 --- a/src/PAModel/PAConvert/ErrorContainer.cs +++ b/src/PAModel/PAConvert/ErrorContainer.cs @@ -14,7 +14,7 @@ namespace Microsoft.PowerPlatform.Formulas.Tools; /// public class ErrorContainer : IEnumerable { - private readonly List _errors = new(); + private readonly List _errors = []; internal void AddError(ErrorCode code, SourceLocation span, string errorMessage) { diff --git a/src/PAModel/PAConvert/Parser/Parser.cs b/src/PAModel/PAConvert/Parser/Parser.cs index 0978f55b..a86b444c 100644 --- a/src/PAModel/PAConvert/Parser/Parser.cs +++ b/src/PAModel/PAConvert/Parser/Parser.cs @@ -55,7 +55,7 @@ internal static bool TryParseControlDefCore(string line, out string ctrlName, ou return false; ctrlName = parsedIdent; - line = line.Substring(length); + line = line[length..]; if (!line.StartsWith(" ")) return false; line = line.TrimStart(); @@ -63,7 +63,7 @@ internal static bool TryParseControlDefCore(string line, out string ctrlName, ou if (!line.StartsWith("As ")) return false; - line = line.Substring(2).TrimStart(); + line = line[2..].TrimStart(); if (!TryParseIdent(line, out parsedIdent, out length)) return false; @@ -73,10 +73,10 @@ internal static bool TryParseControlDefCore(string line, out string ctrlName, ou if (length == line.Length) return true; - line = line.Substring(length); + line = line[length..]; if (!line.StartsWith(".")) return false; - line = line.Substring(1); + line = line[1..]; if (!TryParseIdent(line, out parsedIdent, out length)) return false; @@ -258,7 +258,7 @@ private FunctionNode ParseFunctionDef(YamlToken p) var funcName = m.Groups[1].Value; var functionNode = new FunctionNode() { Identifier = funcName }; - line = line.Substring(m.Length); + line = line[m.Length..]; m = paramRegex.Match(line); while (m.Success) @@ -275,7 +275,7 @@ private FunctionNode ParseFunctionDef(YamlToken p) } }); - line = line.Substring(m.Length).TrimStart(',', ' '); + line = line[m.Length..].TrimStart(',', ' '); m = paramRegex.Match(line); } @@ -343,7 +343,7 @@ private static bool IsControlStart(string line) { if (!TryParseIdent(line, out _, out var length)) return false; - line = line.Substring(length).TrimStart(); + line = line[length..].TrimStart(); return line.StartsWith("As"); } diff --git a/src/PAModel/PAConvert/Parser/SourceLocation.cs b/src/PAModel/PAConvert/Parser/SourceLocation.cs index aa8861f6..1cdf1e6b 100644 --- a/src/PAModel/PAConvert/Parser/SourceLocation.cs +++ b/src/PAModel/PAConvert/Parser/SourceLocation.cs @@ -6,27 +6,14 @@ namespace Microsoft.PowerPlatform.Formulas.Tools.IR; -internal readonly struct SourceLocation +/// +/// Indices into file are 1-based. +/// +internal readonly record struct SourceLocation(int StartLine, int StartChar, int EndLine, int EndChar, string FileName) { - public readonly int StartLine; - public readonly int StartChar; - public readonly int EndLine; - public readonly int EndChar; - public readonly string FileName; - - // Indices into file are 1-based. - public SourceLocation(int startLine, int startChar, int endLine, int endChar, string fileName) - { - StartLine = startLine; - StartChar = startChar; - EndLine = endLine; - EndChar = endChar; - FileName = fileName; - } - public static SourceLocation FromFile(string filename) { - return new SourceLocation(0, 0, 0, 0, filename); + return new(0, 0, 0, 0, filename); } public override string ToString() @@ -84,19 +71,4 @@ public static SourceLocation FromChildren(List locations) return new SourceLocation(minLoc.StartLine, minLoc.StartChar, maxLoc.EndLine, maxLoc.EndChar, maxLoc.FileName); } - - public override bool Equals(object obj) - { - return obj is SourceLocation other && - other.FileName == FileName && - other.StartChar == StartChar && - other.StartLine == StartLine && - other.EndChar == EndChar && - other.EndLine == EndLine; - } - - public override int GetHashCode() - { - return (FileName, StartChar, EndChar, StartLine, EndLine).GetHashCode(); - } } diff --git a/src/PAModel/PAConvert/Yaml/YamlLexer.cs b/src/PAModel/PAConvert/Yaml/YamlLexer.cs index 0105d6e4..53eebbb2 100644 --- a/src/PAModel/PAConvert/Yaml/YamlLexer.cs +++ b/src/PAModel/PAConvert/Yaml/YamlLexer.cs @@ -16,7 +16,7 @@ namespace Microsoft.PowerPlatform.Formulas.Tools.Yaml; /// internal class YamlLexer : IDisposable { - private const string NewLine = "\n"; + private const char NewLine = '\n'; // The actual contents to read. private readonly TextReader _reader; @@ -49,13 +49,12 @@ public YamlLexer(TextReader source, string filenameHint = null) // We pretend the file is wrapped in a "object:" tag. _lastPair = YamlToken.NewStartObj(SourceLocation.FromFile(_currentFileName), null); - _currentIndent = - [ + _currentIndent = new([ new() { LineStart = 0, OldIndentLevel = -1, } - ]; + ]); } /// @@ -264,7 +263,7 @@ private YamlToken ReadNextWorker() line.MaybeEat(':'); // skip colon. // Prop name could have spaces, but no colons. - var propName = line._line.Substring(indentLen, line._idx - indentLen - 1).Trim(); + var propName = line.Line.Substring(indentLen, line._idx - indentLen - 1).Trim(); if (requiresClosingDoubleQuote) { @@ -321,7 +320,7 @@ private YamlToken ReadNextWorker() // Single line. Property doesn't include \n at end. value = line.RestOfLine; - if (value.IndexOf('#') >= 0) + if (value.Contains('#')) { return UnsupportedComment(line); } @@ -392,7 +391,7 @@ private YamlToken ReadNextWorker() return Unsupported(line, "Property value must start with an '='"); } - value = value.Substring(1); // move past '=' + value = value[1..]; // move past '=' } else if (Options.HasFlag(YamlLexerOptions.EnforceLeadingEquals)) { @@ -406,7 +405,7 @@ private YamlToken ReadNextWorker() MoveNextLine(); } - var endIndex = line._line.Length + 1; + var endIndex = line.Line.Length + 1; return YamlToken.NewProperty(LocWorker(startColumn, endIndex), propName, value); } @@ -419,7 +418,7 @@ private YamlToken Unsupported(LineParser line, string message) private YamlToken UnsupportedSingleQuote(LineParser line, bool isComponent) { - var lineSplit = line._line.ToString().ToLower().Split(new string[] { " as " }, StringSplitOptions.None); + var lineSplit = line.Line.ToLower().Split(" as ", StringSplitOptions.None); if (lineSplit.Length > 2) { @@ -543,46 +542,19 @@ private string ReadMultiline(int multilineMode) // Helper for reading through a single line. // This treats EOL as (char) 0. [DebuggerDisplay("{DebuggerToString()}")] - private class LineParser + private class LineParser(string line) { // 0-based character index into line public int _idx; - public readonly string _line; - - public LineParser(string line) - { - _line = line; - } + public readonly string Line = line; // Helper to handle eol. - public char Current - { - get - { - if (_idx >= _line.Length) { return (char)0; } - return _line[_idx]; - } - } - public char Previous - { - get - { - if (_idx - 1 >= _line.Length || _idx - 1 < 0) { return (char)0; } - return _line[_idx - 1]; - } - } - + public char Current => _idx >= Line.Length ? (char)0 : Line[_idx]; - public string RestOfLine - { - get - { - if (_idx >= _line.Length) return string.Empty; - return _line.Substring(_idx); - } - } + public char Previous => _idx - 1 >= Line.Length || _idx - 1 < 0 ? (char)0 : Line[_idx - 1]; + public string RestOfLine => _idx >= Line.Length ? string.Empty : Line[_idx..]; public bool MaybeEat(char ch) { @@ -605,8 +577,8 @@ public int EatIndent() // Show '|' at _idx position private string DebuggerToString() { - var idx = Math.Min(_line.Length, _idx); - return _line.Substring(0, idx) + "¤" + _line.Substring(idx); + var idx = Math.Min(Line.Length, _idx); + return $"{Line[..idx]}¤{Line[idx..]}"; } } diff --git a/src/PAModel/PAConvert/Yaml/YamlWriter.cs b/src/PAModel/PAConvert/Yaml/YamlWriter.cs index a7509873..d60b9fd2 100644 --- a/src/PAModel/PAConvert/Yaml/YamlWriter.cs +++ b/src/PAModel/PAConvert/Yaml/YamlWriter.cs @@ -32,8 +32,7 @@ public void WriteStartObject(string propertyName) { WriteIndent(); - var needsEscape = propertyName.IndexOfAny(new char[] { '\"', '\'' }) != -1; - if (needsEscape) + if (propertyName.AsSpan().ContainsAny("\"'")) propertyName = $"\"{propertyName.Replace("\"", "\\\"")}\""; _textWriter.Write(propertyName); @@ -106,7 +105,7 @@ public void WriteProperty(string propertyName, string value, bool includeEquals value = NormalizeNewlines(value); - var isSingleLine = value.IndexOfAny(new char[] { '#', '\n', ':' }) == -1; + var isSingleLine = value.IndexOfAny(['#', '\n', ':']) == -1; // For consistency, both single and multiline PA properties prefix with '='. // Only single-line actually needs this - to avoid yaml's regular expression escaping. diff --git a/src/PAModel/Schemas/DataComponentDefinitionJson.cs b/src/PAModel/Schemas/DataComponentDefinitionJson.cs index c494a508..15167a90 100644 --- a/src/PAModel/Schemas/DataComponentDefinitionJson.cs +++ b/src/PAModel/Schemas/DataComponentDefinitionJson.cs @@ -88,8 +88,10 @@ public ComponentDefinitionInfoJson(ControlInfoJson.Item item, string timestamp, // Once ControlPropertyState has an actual schema, this can be cleaned up. if (item.ExtensionData.ContainsKey("ControlPropertyState")) { - ExtensionData = new Dictionary(); - ExtensionData["ControlPropertyState"] = item.ExtensionData["ControlPropertyState"]; + ExtensionData = new Dictionary + { + ["ControlPropertyState"] = item.ExtensionData["ControlPropertyState"] + }; } } } diff --git a/src/PAModel/Schemas/DocumentPropertiesJson.cs b/src/PAModel/Schemas/DocumentPropertiesJson.cs index 4603a8c4..9de2dcde 100644 --- a/src/PAModel/Schemas/DocumentPropertiesJson.cs +++ b/src/PAModel/Schemas/DocumentPropertiesJson.cs @@ -71,7 +71,7 @@ public static DocumentPropertiesJson CreateDefault(string name) Author = "", FileID = Guid.NewGuid().ToString(), Id = Guid.NewGuid().ToString(), - ControlCount = new Dictionary(), + ControlCount = [], DefaultConnectedDataSourceMaxGetRowsCount = 500, DocumentAppType = AppType.DesktopOrTablet, @@ -96,8 +96,8 @@ public static DocumentPropertiesJson CreateDefault(string name) private static string[] GetAppPreviewFlagDefault() { - return new string[] - { + return + [ "delayloadscreens", "blockmovingcontrol", "projectionmapping", @@ -116,7 +116,7 @@ private static string[] GetAppPreviewFlagDefault() "aibuilderserviceenrollment", "enablesummerlandgeospatialfeatures", "enablesummerlandmixedrealityfeatures" - }; + ]; } } diff --git a/src/PAModel/Schemas/PcfControls/PcfControl.cs b/src/PAModel/Schemas/PcfControls/PcfControl.cs index 9877de07..d4a1acf0 100644 --- a/src/PAModel/Schemas/PcfControls/PcfControl.cs +++ b/src/PAModel/Schemas/PcfControls/PcfControl.cs @@ -95,7 +95,7 @@ internal static string GenerateDynamicControlDefinition(PcfControl control) _dynamicControlDefinition.SubscribedFunctionalities = control.SubscribedFunctionalities != null ? JsonSerializer.Serialize(control.SubscribedFunctionalities, jsonOptions) : null; _dynamicControlDefinition.AuthConfigProperties = control.AuthConfigProperties != null ? JsonSerializer.Serialize(control.AuthConfigProperties, jsonOptions) : null; _dynamicControlDefinition.DataConnectors = control.DataConnectors != null ? JsonSerializer.Serialize(control.DataConnectors, jsonOptions) : null; - _dynamicControlDefinition.ExtensionData = control.ExtensionData ?? new Dictionary(); + _dynamicControlDefinition.ExtensionData = control.ExtensionData ?? []; return JsonExtensions.JsonSerialize(_dynamicControlDefinition); } diff --git a/src/PAModel/Schemas/ResourcesJson.cs b/src/PAModel/Schemas/ResourcesJson.cs index 78bcdba2..18b23bdf 100644 --- a/src/PAModel/Schemas/ResourcesJson.cs +++ b/src/PAModel/Schemas/ResourcesJson.cs @@ -66,7 +66,7 @@ internal class ResourceJson public FilePath GetPath() { - var resourceName = Path.Substring("Assets\\".Length); + var resourceName = Path["Assets\\".Length..]; var path = FilePath.FromMsAppPath(resourceName); return path; } diff --git a/src/PAModel/Schemas/adhoc/CanvasManifestJson.cs b/src/PAModel/Schemas/adhoc/CanvasManifestJson.cs index 2d333a3f..1a7ecf0c 100644 --- a/src/PAModel/Schemas/adhoc/CanvasManifestJson.cs +++ b/src/PAModel/Schemas/adhoc/CanvasManifestJson.cs @@ -21,5 +21,5 @@ internal class CanvasManifestJson // Logo file // $$$ Other files? public PublishInfoJson PublishInfo { get; set; } - public List ScreenOrder { get; set; } = new List(); + public List ScreenOrder { get; set; } = []; } diff --git a/src/PAModel/Schemas/adhoc/Control.cs b/src/PAModel/Schemas/adhoc/Control.cs index 8769b3d0..72ce5a8f 100644 --- a/src/PAModel/Schemas/adhoc/Control.cs +++ b/src/PAModel/Schemas/adhoc/Control.cs @@ -93,11 +93,13 @@ public static Template CreateDefaultTemplate(string name, ControlTemplate contro template.Version = controlTemplate.Version; template.IsComponentDefinition = false; template.LastModifiedTimestamp = "0"; - template.ExtensionData = new Dictionary(); - template.ExtensionData.Add("FirstParty", true); - template.ExtensionData.Add("IsCustomGroupControlTemplate", false); - template.ExtensionData.Add("CustomGroupControlTemplateName", ""); - template.ExtensionData.Add("OverridableProperties", new object()); + template.ExtensionData = new Dictionary + { + { "FirstParty", true }, + { "IsCustomGroupControlTemplate", false }, + { "CustomGroupControlTemplateName", "" }, + { "OverridableProperties", new object() } + }; } return template; } @@ -173,14 +175,16 @@ public static Item CreateDefaultControl(ControlTemplate templateDefault = null) public static Dictionary CreateDefaultExtensionData() { - var extensionData = new Dictionary(); - extensionData.Add("LayoutName", ""); - extensionData.Add("MetaDataIDKey", ""); - extensionData.Add("PersistMetaDataIDKey", false); - extensionData.Add("IsFromScreenLayout", false); - extensionData.Add("IsDataControl", false); - extensionData.Add("IsAutoGenerated", false); - extensionData.Add("IsLocked", false); + var extensionData = new Dictionary + { + { "LayoutName", "" }, + { "MetaDataIDKey", "" }, + { "PersistMetaDataIDKey", false }, + { "IsFromScreenLayout", false }, + { "IsDataControl", false }, + { "IsAutoGenerated", false }, + { "IsLocked", false } + }; return extensionData; } } diff --git a/src/PAModel/Serializers/MsAppSerializer.cs b/src/PAModel/Serializers/MsAppSerializer.cs index 1df71654..ef56969a 100644 --- a/src/PAModel/Serializers/MsAppSerializer.cs +++ b/src/PAModel/Serializers/MsAppSerializer.cs @@ -122,7 +122,7 @@ public static CanvasDocument Load(Stream streamToMsapp, ErrorContainer errors) break; case FileKind.Asset: - app.AddAssetFile(FileEntry.FromZip(entry, name: fullName.Substring("Assets\\".Length))); + app.AddAssetFile(FileEntry.FromZip(entry, name: fullName["Assets\\".Length..])); break; case FileKind.Checksum: @@ -675,7 +675,7 @@ private static IEnumerable GetMsAppFiles(this CanvasDocument app, Err var idRestorer = new UniqueIdRestorer(app._entropy); - var maxPublishOrderIndex = app._entropy.PublishOrderIndices.Any() ? app._entropy.PublishOrderIndices.Values.Max() : 0; + var maxPublishOrderIndex = app._entropy.PublishOrderIndices.Count != 0 ? app._entropy.PublishOrderIndices.Values.Max() : 0; // Rehydrate sources before yielding any to be written, processing component definitions first foreach (var controlData in app._screens.Concat(app._components) .OrderBy(source => @@ -724,7 +724,7 @@ private static IEnumerable GetMsAppFiles(this CanvasDocument app, Err } } - RepairComponentInstanceIndex(app._entropy?.ComponentIndexes ?? new Dictionary(), sourceFiles); + RepairComponentInstanceIndex(app._entropy?.ComponentIndexes ?? [], sourceFiles); // This ordering is essential, we need to match the order in which Studio writes the files to replicate certain order-dependent behavior. @@ -746,7 +746,7 @@ private static IEnumerable GetMsAppFiles(this CanvasDocument app, Err var pcfTemplates = app._templates.PcfTemplates ?? Array.Empty(); app._templates = new TemplatesJson() { - ComponentTemplates = componentTemplates.Any() ? componentTemplates.OrderBy(app._entropy.GetComponentOrder).ToArray() : null, + ComponentTemplates = componentTemplates.Count != 0 ? componentTemplates.OrderBy(app._entropy.GetComponentOrder).ToArray() : null, UsedTemplates = app._templates.UsedTemplates.OrderBy(app._entropy.GetOrder).ToArray(), PcfTemplates = pcfTemplates.Any() ? pcfTemplates.OrderBy(app._entropy.GetPcfVersioning).ToArray() : null }; @@ -921,7 +921,7 @@ internal static FileEntry ToFile(FileKind kind, T value) } else { - var jsonStr = JsonSerializer.Serialize(value, JsonExtensions._jsonOpts); + var jsonStr = JsonSerializer.Serialize(value, JsonExtensions.JsonOpts); output = JsonNormalizer.Normalize(jsonStr); } diff --git a/src/PAModel/Serializers/SourceSerializer.cs b/src/PAModel/Serializers/SourceSerializer.cs index 2c8e87e0..02feb14e 100644 --- a/src/PAModel/Serializers/SourceSerializer.cs +++ b/src/PAModel/Serializers/SourceSerializer.cs @@ -307,12 +307,12 @@ public static CanvasDocument Create(string appName, string packagesPath, IList loadedTemplates) { - loadedTemplates = new Dictionary(); + loadedTemplates = []; var templateList = new List(); foreach (var file in new DirectoryReader(packagesPath).EnumerateFiles(string.Empty, "*.xml", searchSubdirectories: false)) { var xmlContents = file.GetContents(); - var templateNameFromFile = file._relativeName.Substring(0, file._relativeName.LastIndexOf('_')); + var templateNameFromFile = file._relativeName[..file._relativeName.LastIndexOf('_')]; if (!ControlTemplateParser.TryParseTemplate(new TemplateStore(), xmlContents, app._properties.DocumentAppType, loadedTemplates, out var parsedTemplate, out var templateName, templateNameFromFile)) { errors.GenericError($"Unable to parse template file {file._relativeName}"); @@ -675,7 +675,7 @@ public static void SaveAsSource(CanvasDocument app, string directory2, ErrorCont private static void WriteDataSources(DirectoryWriter dir, CanvasDocument app, ErrorContainer errors) { - var untrackedLdr = app._dataSourceReferences?.Select(x => x.Key)?.ToList() ?? new List(); + var untrackedLdr = app._dataSourceReferences?.Select(x => x.Key)?.ToList() ?? []; // Data Sources - write out each individual source. var filenames = new HashSet(); @@ -751,7 +751,7 @@ private static void WriteDataSources(DirectoryWriter dir, CanvasDocument app, Er } if (ds.WadlMetadata.SwaggerJson != null) { - dir.WriteAllJson(SwaggerPackageDir, new FilePath(filename), JsonSerializer.Deserialize(ds.WadlMetadata.SwaggerJson, JsonExtensions._jsonOpts)); + dir.WriteAllJson(SwaggerPackageDir, new FilePath(filename), JsonSerializer.Deserialize(ds.WadlMetadata.SwaggerJson, JsonExtensions.JsonOpts)); } ds.WadlMetadata = null; } @@ -890,7 +890,7 @@ private static void LoadDataSources(CanvasDocument app, DirectoryReader director // now that we have seen first non null LocalReferenceDSJson // we know for sure that dataSources for the localDatabaseReferenceJson was not null // in that case, no longer assume dataSource to be null - localDatabaseReferenceJson.dataSources ??= new Dictionary(); + localDatabaseReferenceJson.dataSources ??= []; localDatabaseReferenceJson.dataSources?.Add(tableDef.EntityName, tableDef.LocalReferenceDSJson); } } @@ -926,7 +926,7 @@ private static void LoadDataSources(CanvasDocument app, DirectoryReader director { case "NativeCDSDataSourceInfo": ds.DatasetName = definition.DatasetName; - ds.TableDefinition = JsonSerializer.Serialize(definition.TableDefinition, JsonExtensions._jsonOpts); + ds.TableDefinition = JsonSerializer.Serialize(definition.TableDefinition, JsonExtensions.JsonOpts); break; case "ConnectedDataSourceInfo": ds.DataEntityMetadataJson = definition.DataEntityMetadataJson; @@ -971,7 +971,7 @@ private static void TrimViewNames(IEnumerable dataSourceEntries { if (ds.Name.StartsWith(dataSetName)) { - ds.Name = ds.Name.Substring(dataSetName.Length); + ds.Name = ds.Name[dataSetName.Length..]; ds.TrimmedViewName = true; } } @@ -1012,7 +1012,7 @@ private static void WriteTopParent( { foreach (var child in ir.Children) { - WriteTopParent(dir, app, child.Properties.FirstOrDefault(x => x.Identifier == "DisplayName").Expression.Expression.Trim(new char[] { '"' }), child, subDir, controlName); + WriteTopParent(dir, app, child.Properties.FirstOrDefault(x => x.Identifier == "DisplayName").Expression.Expression.Trim(['"']), child, subDir, controlName); } // Clear the children since they have already been sharded into their individual files. @@ -1059,7 +1059,7 @@ private static void AddDefaultTheme(CanvasDocument app) var jsonString = reader.ReadToEnd(); - app._themes = JsonSerializer.Deserialize(jsonString, JsonExtensions._jsonOpts); + app._themes = JsonSerializer.Deserialize(jsonString, JsonExtensions.JsonOpts); } private static BuildVerJson GetBuildDetails() @@ -1076,7 +1076,7 @@ private static BuildVerJson GetBuildDetails() using var reader = new StreamReader(stream); var jsonString = reader.ReadToEnd(); - return JsonSerializer.Deserialize(jsonString, JsonExtensions._jsonOpts); + return JsonSerializer.Deserialize(jsonString, JsonExtensions.JsonOpts); } catch (Exception) { diff --git a/src/PAModel/SourceTransforms/AppTestTransform.cs b/src/PAModel/SourceTransforms/AppTestTransform.cs index 4c3a46f4..04ec6690 100644 --- a/src/PAModel/SourceTransforms/AppTestTransform.cs +++ b/src/PAModel/SourceTransforms/AppTestTransform.cs @@ -241,6 +241,6 @@ public void BeforeWrite(BlockNode control) Identifier = _metadataPropName }); } - control.Children = new List(); + control.Children = []; } } diff --git a/src/PAModel/SourceTransforms/ComponentInstanceTransform.cs b/src/PAModel/SourceTransforms/ComponentInstanceTransform.cs index a6213cc2..dc272b38 100644 --- a/src/PAModel/SourceTransforms/ComponentInstanceTransform.cs +++ b/src/PAModel/SourceTransforms/ComponentInstanceTransform.cs @@ -10,7 +10,7 @@ internal class ComponentInstanceTransform : IControlTemplateTransform // Key is name in source, value is name in target // For AfterRead, that's ComponentID => ComponentName // For BeforeWrite, that's ComponentName => ComponentID - internal Dictionary ComponentRenames = new(); + internal Dictionary ComponentRenames = []; private readonly ErrorContainer _errors; public ComponentInstanceTransform(ErrorContainer errors) diff --git a/src/PAModel/SourceTransforms/DefaultValuesTransform.cs b/src/PAModel/SourceTransforms/DefaultValuesTransform.cs index 82ed9158..c541e287 100644 --- a/src/PAModel/SourceTransforms/DefaultValuesTransform.cs +++ b/src/PAModel/SourceTransforms/DefaultValuesTransform.cs @@ -58,8 +58,8 @@ public void BeforeWrite(BlockNode node, bool inResponsiveContext) if (_controlStore.TryGetControlState(controlName, out var controlState) && controlState.Properties != null) { styleName = controlState.StyleName; - propNames = new HashSet(controlState.Properties.Select(state => state.PropertyName) - .Concat(controlState.DynamicProperties?.Where(state => state.Property != null).Select(state => state.PropertyName) ?? Enumerable.Empty())); + propNames = [.. controlState.Properties.Select(state => state.PropertyName) + .Concat(controlState.DynamicProperties?.Where(state => state.Property != null).Select(state => state.PropertyName) ?? Enumerable.Empty())]; } if (!_templateStore.TryGetValue(templateName, out var template)) diff --git a/src/PAModel/SourceTransforms/GalleryTemplateTransform.cs b/src/PAModel/SourceTransforms/GalleryTemplateTransform.cs index dc3a10e7..cbac6246 100644 --- a/src/PAModel/SourceTransforms/GalleryTemplateTransform.cs +++ b/src/PAModel/SourceTransforms/GalleryTemplateTransform.cs @@ -65,8 +65,8 @@ public void BeforeWrite(BlockNode control) TypeName = _childTemplateName } }, - Children = new List(), - Functions = new List(), + Children = [], + Functions = [], Properties = childRules }; diff --git a/src/PAModel/SourceTransforms/GroupControlTransform.cs b/src/PAModel/SourceTransforms/GroupControlTransform.cs index 3796ee97..277d8dbe 100644 --- a/src/PAModel/SourceTransforms/GroupControlTransform.cs +++ b/src/PAModel/SourceTransforms/GroupControlTransform.cs @@ -34,7 +34,7 @@ public GroupControlTransform(ErrorContainer errors, EditorStateStore editorState public void AfterRead(BlockNode control) { var groupControls = GetGroupControlChildren(control); - if (!groupControls.Any()) + if (groupControls.Count == 0) return; var peerControlsDict = control.Children.ToDictionary(peer => peer.Name.Identifier, peer => peer); @@ -74,7 +74,7 @@ public void AfterRead(BlockNode control) public void BeforeWrite(BlockNode control) { var groupControls = GetGroupControlChildren(control); - if (!groupControls.Any()) + if (groupControls.Count == 0) return; foreach (var groupControl in groupControls) @@ -102,7 +102,7 @@ public void BeforeWrite(BlockNode control) { control.Children.Add(child); } - groupControl.Children = new List(); + groupControl.Children = []; groupControlState.GroupedControlsKey = groupedControlNames.ToList(); } } diff --git a/src/PAModel/SourceTransforms/SourceTransformer.cs b/src/PAModel/SourceTransforms/SourceTransformer.cs index 72b04242..817c3b17 100644 --- a/src/PAModel/SourceTransforms/SourceTransformer.cs +++ b/src/PAModel/SourceTransforms/SourceTransformer.cs @@ -29,10 +29,12 @@ public SourceTransformer( TemplateStore templateStore, Entropy entropy) { - _templateTransforms = new List(); - _templateTransforms.Add(new GalleryTemplateTransform(defaultValueTemplates, stateStore)); - _templateTransforms.Add(new AppTestTransform(app, errors, templateStore, stateStore, entropy)); - _templateTransforms.Add(componentInstanceTransform); + _templateTransforms = + [ + new GalleryTemplateTransform(defaultValueTemplates, stateStore), + new AppTestTransform(app, errors, templateStore, stateStore, entropy), + componentInstanceTransform, + ]; _groupControlTransform = new GroupControlTransform(errors, stateStore, entropy); diff --git a/src/PAModel/packages.lock.json b/src/PAModel/packages.lock.json index 09f295cd..a8cc0678 100644 --- a/src/PAModel/packages.lock.json +++ b/src/PAModel/packages.lock.json @@ -8,11 +8,11 @@ "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, - "System.IO.Compression": { + "PolySharp": { "type": "Direct", - "requested": "[4.3.0, )", - "resolved": "4.3.0", - "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==" + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "FbU0El+EEjdpuIX4iDbeS7ki1uzpJPx8vbqOzEtqnl1GZeAGJfq+jCbxeJL2y0EPnUNk8dRnnqR2xnYXg9Tf+g==" }, "System.Text.Encodings.Web": { "type": "Direct", @@ -25,22 +25,6 @@ "System.Runtime.CompilerServices.Unsafe": "6.1.2" } }, - "System.Text.Json": { - "type": "Direct", - "requested": "[10.0.7, )", - "resolved": "10.0.7", - "contentHash": "F8Pu2QLUMeniVbtiyk7n7LCfFYxlcJ8ASaSwglJyq6dxa34iCQrikQszsgJClIJWuSWjcyhKkV7daAzYJqeVwA==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "10.0.7", - "System.Buffers": "4.6.1", - "System.IO.Pipelines": "10.0.7", - "System.Memory": "4.6.3", - "System.Runtime.CompilerServices.Unsafe": "6.1.2", - "System.Text.Encodings.Web": "10.0.7", - "System.Threading.Tasks.Extensions": "4.6.3", - "System.ValueTuple": "4.6.2" - } - }, "YamlDotNet": { "type": "Direct", "requested": "[17.1.0, )", @@ -60,6 +44,15 @@ "resolved": "4.6.1", "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "10.0.7", + "contentHash": "Fu6AxFf9bHz/Q7DQmxKC0o+UgFes8bs2Xh+PH/x31yExRAOASTwlzjZsISTtqVU5gQshKHLZopxEBTaIyfv0wg==", + "dependencies": { + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, "System.IO.Pipelines": { "type": "Transitive", "resolved": "10.0.7", @@ -85,6 +78,57 @@ "resolved": "4.6.2", "contentHash": "yQgmjfFximrNm9LIV3mL6T5MzjeC+epeE5rl4hXxAlYmxby7RM1dPSkIKXk9HNkl6G54h2JHOmLD46+Pey+IRg==" }, + "microsoft.powerplatform.powerapps.persistence": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.7, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.7, )", + "System.Collections.Immutable": "[10.0.7, )", + "System.IO.Compression": "[4.3.0, )", + "System.Memory": "[4.6.3, )", + "System.Text.Json": "[10.0.7, )", + "System.Threading.Tasks.Extensions": "[4.6.3, )", + "YamlDotNet": "[17.1.0, )" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.7, )", + "resolved": "10.0.7", + "contentHash": "Z6mfFEaFcwCfSboxJwOLfu7/31npCY9q70WUamHW/vRQhDvBKOT4Vf9YkZj5J6hLvJpb0oDEYfHunQZj0xxvKw==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "10.0.7", + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.7, )", + "resolved": "10.0.7", + "contentHash": "tIEcQ2gvERrH2KiCjdsVcHGhXt9lIsuDStfOIeZWr7/fP8IXhGiYfx0/80PNI7WPO2IYuFtlZLSlnTS8+/Mchw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7", + "System.Buffers": "4.6.1", + "System.Diagnostics.DiagnosticSource": "10.0.7", + "System.Memory": "4.6.3" + } + }, + "System.Collections.Immutable": { + "type": "CentralTransitive", + "requested": "[10.0.7, )", + "resolved": "10.0.7", + "contentHash": "0Ti4Jv1ga3eurH5HaCVsPybcBl+08YfzM9smqAJzHvqV494xK+0pSbytGrMTWhph+zsyKIaoGNiR5u3by5bj+A==", + "dependencies": { + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "System.IO.Compression": { + "type": "CentralTransitive", + "requested": "[4.3.0, )", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==" + }, "System.Memory": { "type": "CentralTransitive", "requested": "[4.6.3, )", @@ -96,6 +140,22 @@ "System.Runtime.CompilerServices.Unsafe": "6.1.2" } }, + "System.Text.Json": { + "type": "CentralTransitive", + "requested": "[10.0.7, )", + "resolved": "10.0.7", + "contentHash": "F8Pu2QLUMeniVbtiyk7n7LCfFYxlcJ8ASaSwglJyq6dxa34iCQrikQszsgJClIJWuSWjcyhKkV7daAzYJqeVwA==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "10.0.7", + "System.Buffers": "4.6.1", + "System.IO.Pipelines": "10.0.7", + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2", + "System.Text.Encodings.Web": "10.0.7", + "System.Threading.Tasks.Extensions": "4.6.3", + "System.ValueTuple": "4.6.2" + } + }, "System.Threading.Tasks.Extensions": { "type": "CentralTransitive", "requested": "[4.6.3, )", @@ -118,6 +178,29 @@ "requested": "[17.1.0, )", "resolved": "17.1.0", "contentHash": "AhsNXgeAs3Ugt653t8LC44xXDuldFfwBpWbWX9pN3e4Yg8U5Bk8jLn8eXtGv5HV2V2nHu7F46fqsPC+tpcTGAA==" + }, + "microsoft.powerplatform.powerapps.persistence": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.7, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.7, )", + "YamlDotNet": "[17.1.0, )" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.7, )", + "resolved": "10.0.7", + "contentHash": "Z6mfFEaFcwCfSboxJwOLfu7/31npCY9q70WUamHW/vRQhDvBKOT4Vf9YkZj5J6hLvJpb0oDEYfHunQZj0xxvKw==" + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.7, )", + "resolved": "10.0.7", + "contentHash": "tIEcQ2gvERrH2KiCjdsVcHGhXt9lIsuDStfOIeZWr7/fP8IXhGiYfx0/80PNI7WPO2IYuFtlZLSlnTS8+/Mchw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7" + } } }, "net8.0": { @@ -132,6 +215,35 @@ "requested": "[17.1.0, )", "resolved": "17.1.0", "contentHash": "AhsNXgeAs3Ugt653t8LC44xXDuldFfwBpWbWX9pN3e4Yg8U5Bk8jLn8eXtGv5HV2V2nHu7F46fqsPC+tpcTGAA==" + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "10.0.7", + "contentHash": "Fu6AxFf9bHz/Q7DQmxKC0o+UgFes8bs2Xh+PH/x31yExRAOASTwlzjZsISTtqVU5gQshKHLZopxEBTaIyfv0wg==" + }, + "microsoft.powerplatform.powerapps.persistence": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.7, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.7, )", + "YamlDotNet": "[17.1.0, )" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.7, )", + "resolved": "10.0.7", + "contentHash": "Z6mfFEaFcwCfSboxJwOLfu7/31npCY9q70WUamHW/vRQhDvBKOT4Vf9YkZj5J6hLvJpb0oDEYfHunQZj0xxvKw==" + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.7, )", + "resolved": "10.0.7", + "contentHash": "tIEcQ2gvERrH2KiCjdsVcHGhXt9lIsuDStfOIeZWr7/fP8IXhGiYfx0/80PNI7WPO2IYuFtlZLSlnTS8+/Mchw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7", + "System.Diagnostics.DiagnosticSource": "10.0.7" + } } } } diff --git a/src/PAModelTests/.editorconfig b/src/PAModelTests/.editorconfig deleted file mode 100644 index bf31e936..00000000 --- a/src/PAModelTests/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -[*.cs] - -# Category overries: -# Disabling these categories because the original implementation didn't have them on and we're not making changes to it at this time. -dotnet_analyzer_diagnostic.category-Globalization.severity = silent -dotnet_analyzer_diagnostic.category-Performance.severity = silent -dotnet_analyzer_diagnostic.category-Usage.severity = silent - -# Rule overrides: diff --git a/src/PAModelTests/PAModelTests.csproj b/src/PAModelTests/PAModelTests.csproj index 840c197f..8f802f8b 100644 --- a/src/PAModelTests/PAModelTests.csproj +++ b/src/PAModelTests/PAModelTests.csproj @@ -2,11 +2,15 @@ net48;net8.0;net10.0 - 10.0 + 13 true true + + + + diff --git a/src/PAModelTests/RoundtripTests.cs b/src/PAModelTests/RoundtripTests.cs index 066580a0..f4c42eb5 100644 --- a/src/PAModelTests/RoundtripTests.cs +++ b/src/PAModelTests/RoundtripTests.cs @@ -18,7 +18,7 @@ private static IEnumerable TestAppFilePaths var appsDirectory = new DirectoryInfo("Apps"); foreach (var file in appsDirectory.EnumerateFiles("*.msapp", SearchOption.AllDirectories)) { - var testAppRelativePath = file.FullName.Substring(Environment.CurrentDirectory.Length + 1); + var testAppRelativePath = file.FullName[(Environment.CurrentDirectory.Length + 1)..]; yield return new object[] { testAppRelativePath }; } diff --git a/src/PAModelTests/YamlTest.cs b/src/PAModelTests/YamlTest.cs index 80dba430..f4ffb99f 100644 --- a/src/PAModelTests/YamlTest.cs +++ b/src/PAModelTests/YamlTest.cs @@ -639,7 +639,7 @@ private static string ParseSinglePropertyViaYamlDotNot(string text) // Strip the '=' that we add. if (val[0] == '=') { - val = val.Substring(1); + val = val[1..]; } return val; } diff --git a/src/PASopa/.editorconfig b/src/PASopa/.editorconfig deleted file mode 100644 index 258b6cae..00000000 --- a/src/PASopa/.editorconfig +++ /dev/null @@ -1,8 +0,0 @@ -[*.cs] - -# Category overries: -# Disabling these categories because the original implementation didn't have them on and we're not making changes to it at this time. -dotnet_analyzer_diagnostic.category-Globalization.severity = silent -dotnet_analyzer_diagnostic.category-Performance.severity = silent - -# Rule overrides: diff --git a/src/Persistence/TfmExtensions/MemoryTfmExtensions.cs b/src/Persistence/TfmExtensions/MemoryTfmExtensions.cs new file mode 100644 index 00000000..a076c74a --- /dev/null +++ b/src/Persistence/TfmExtensions/MemoryTfmExtensions.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#nullable enable + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.TfmExtensions; + +public static class MemoryTfmExtensions +{ +#if !NET9_0_OR_GREATER + public static bool ContainsAny(this ReadOnlySpan span, string values) + { + return span.ContainsAny(values.AsSpan()); + } + + public static bool ContainsAny(this ReadOnlySpan span, ReadOnlySpan values) + { + return span.IndexOfAny(values) >= 0; + } +#endif +} diff --git a/src/Persistence/TfmExtensions/StringTfmExtensions.cs b/src/Persistence/TfmExtensions/StringTfmExtensions.cs index b0770fd9..4894316b 100644 --- a/src/Persistence/TfmExtensions/StringTfmExtensions.cs +++ b/src/Persistence/TfmExtensions/StringTfmExtensions.cs @@ -10,6 +10,11 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.TfmExtensions; public static class StringTfmExtensions { #if NETFRAMEWORK + public static bool Contains(this string source, char value) + { + return source.Contains(value.ToString()); + } + public static string Replace(this string source, string oldValue, string? newValue, StringComparison comparisonType) { // For now, looks like NetFx only supports the Ordinal comparison for Replace @@ -26,6 +31,11 @@ public static bool EndsWith(this string source, char value) { return source.Length > 0 && source[^1] == value; } + + public static string[] Split(this string source, string? separator, StringSplitOptions options = StringSplitOptions.None) + { + return source.Split([separator], options); + } #endif #if !NET6_0_OR_GREATER