diff --git a/Directory.Packages.props b/Directory.Packages.props index 2ffc34f..b38ba10 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,17 +5,17 @@ - - - + + + - - - - - - + + + + + + @@ -47,7 +47,7 @@ - + diff --git a/Documentation/reference/screenplay.md b/Documentation/reference/screenplay.md index f7bf74b..6be48c6 100644 --- a/Documentation/reference/screenplay.md +++ b/Documentation/reference/screenplay.md @@ -154,14 +154,14 @@ cratis screenplay validate ./plays # every .play file beneath a folder ### Compiler diagnostics -Diagnostics go to **standard error**, grouped by severity with errors first, in the same shape `generate` uses. The compiler does not assign codes, so each line carries the file and the position within it instead: +Diagnostics go to **standard error**, grouped by severity with errors first, in the same shape `generate` uses. Each line carries the compiler's `PLAY` code, then the file and the position within it: ```text errors (1): - error: [MyApp.play(5,5)] Invalid slice declaration 'slice Reserving' - expected 'slice ' + error PLAY0027: [MyApp.play(5,5)] Invalid slice declaration 'slice Reserving' - expected 'slice ' warnings (1): - warning: [MyApp.play(787,11)] Unknown event 'InvitationToJoinAdaAccepted' - declare it with 'event InvitationToJoinAdaAccepted' + warning PLAY0166: [MyApp.play(787,11)] Unknown event 'InvitationToJoinAdaAccepted' - declare it with 'event InvitationToJoinAdaAccepted' ``` With `-o json` or `-o json-compact` the same diagnostics are written to standard error as a JSON object instead. diff --git a/Source/Cli.Specs/for_ArcScreenplayGeneration/given/an_application_built_from_source.cs b/Source/Cli.Specs/for_ArcScreenplayGeneration/given/an_application_built_from_source.cs new file mode 100644 index 0000000..450d8e0 --- /dev/null +++ b/Source/Cli.Specs/for_ArcScreenplayGeneration/given/an_application_built_from_source.cs @@ -0,0 +1,71 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Cratis.Cli.for_ArcScreenplayGeneration.given; + +/// +/// Builds an application from source, so that generating from it runs the real generator rather than a substitute. +/// +/// +/// Everything the CLI knows about the Cratis.Arc.Screenplay generator and the Cratis.Screenplay +/// compiler it holds through package references, and a mismatched pair of the two compiles clean and specs green +/// and then throws the first time a document is generated for real — the +/// generator is built against the compiler's syntax types, and those are positional records whose constructors +/// change shape between major versions. Nothing short of generating catches that. +/// +/// The source declares the two attributes the generator looks for rather than referencing Arc and Chronicle to get +/// them. The generator resolves them by full name, so declaring them is enough to describe an application, and it +/// keeps the compilation to one syntax tree and no package restore. +/// +/// +public class an_application_built_from_source : Specification +{ + /// + /// The name the compilation, and therefore the project the document is generated from, goes by. + /// + protected const string ProjectName = "Bookshop"; + + const string Source = + "namespace Cratis.Arc.Commands.ModelBound { public class CommandAttribute : System.Attribute { } }\n" + + "namespace Cratis.Chronicle.Events { public class EventTypeAttribute : System.Attribute { } }\n" + + "\n" + + "namespace Bookshop.Lending.Reserving\n" + + "{\n" + + " [Cratis.Chronicle.Events.EventType]\n" + + " public record BookReserved(string MemberId);\n" + + "\n" + + " [Cratis.Arc.Commands.ModelBound.Command]\n" + + " public record ReserveBook(string BookId);\n" + + "}\n"; + + /// + /// Gets what loading the application would have produced. + /// + protected LoadedCompilation Loaded { get; private set; } + + void Establish() => Loaded = new( + [CSharpCompilation.Create( + ProjectName, + [CSharpSyntaxTree.ParseText(Source)], + References(), + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))], + [ProjectName], + []); + + /// + /// Gets the references the source needs to compile. + /// + /// The for every assembly this process was started with. + /// + /// A compilation without references resolves nothing, not even , and the generator reports + /// source that did not compile rather than the application it describes. Taking what this process already runs + /// against is enough, and keeps the fixture from naming individual framework assemblies. + /// + static IEnumerable References() => + ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!) + .Split(Path.PathSeparator) + .Select(assembly => MetadataReference.CreateFromFile(assembly)); +} diff --git a/Source/Cli.Specs/for_ArcScreenplayGeneration/when_generating/and_the_source_declares_a_command_and_an_event.cs b/Source/Cli.Specs/for_ArcScreenplayGeneration/when_generating/and_the_source_declares_a_command_and_an_event.cs new file mode 100644 index 0000000..ef93d04 --- /dev/null +++ b/Source/Cli.Specs/for_ArcScreenplayGeneration/when_generating/and_the_source_declares_a_command_and_an_event.cs @@ -0,0 +1,17 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Cli.for_ArcScreenplayGeneration.when_generating; + +public class and_the_source_declares_a_command_and_an_event : given.an_application_built_from_source +{ + GeneratedScreenplay _result; + + void Because() => _result = ArcScreenplayGeneration.GenerateFrom(Loaded, $"{ProjectName}.csproj", ScreenplayGenerationOptions.Default); + + [Fact] void should_say_which_project_it_read() => _result.Projects.ShouldContainOnly([ProjectName]); + [Fact] void should_arrange_the_namespace_into_a_feature_and_a_slice() => _result.Source.ShouldContain("feature Lending"); + [Fact] void should_describe_the_command() => _result.Source.ShouldContain("command ReserveBook"); + [Fact] void should_describe_the_event() => _result.Source.ShouldContain("event BookReserved"); + [Fact] void should_not_report_anything() => _result.Diagnostics.ShouldBeEmpty(); +} diff --git a/Source/Cli.Specs/for_ScreenplayValidation/when_validating/and_the_document_has_an_error.cs b/Source/Cli.Specs/for_ScreenplayValidation/when_validating/and_the_document_has_an_error.cs index 8c137ea..1a56a26 100644 --- a/Source/Cli.Specs/for_ScreenplayValidation/when_validating/and_the_document_has_an_error.cs +++ b/Source/Cli.Specs/for_ScreenplayValidation/when_validating/and_the_document_has_an_error.cs @@ -15,5 +15,5 @@ public class and_the_document_has_an_error : given.a_folder_with_documents [Fact] void should_compile_the_document() => _result.FileCount.ShouldEqual(1); [Fact] void should_report_an_error() => ScreenplayDiagnostics.HasErrors(_result.Diagnostics).ShouldBeTrue(); [Fact] void should_point_at_the_file_and_position() => _result.Diagnostics[0].Location.ShouldEqual("MyApp.play(5,5)"); - [Fact] void should_not_invent_a_code() => _result.Diagnostics[0].Code.ShouldBeEmpty(); + [Fact] void should_carry_the_code_the_compiler_assigned() => _result.Diagnostics[0].Code.ShouldEqual("PLAY0027"); } diff --git a/Source/Cli/Commands/Screenplay/ArcScreenplayGeneration.cs b/Source/Cli/Commands/Screenplay/ArcScreenplayGeneration.cs index f3c187c..47b758d 100644 --- a/Source/Cli/Commands/Screenplay/ArcScreenplayGeneration.cs +++ b/Source/Cli/Commands/Screenplay/ArcScreenplayGeneration.cs @@ -16,9 +16,23 @@ namespace Cratis.Cli.Commands.Screenplay; public sealed class ArcScreenplayGeneration : IScreenplayGeneration { /// - public async Task Generate(string targetPath, ScreenplayGenerationOptions options, CancellationToken cancellationToken) + public async Task Generate(string targetPath, ScreenplayGenerationOptions options, CancellationToken cancellationToken) => + GenerateFrom(await ScreenplayCompilationLoader.Load(targetPath, cancellationToken), targetPath, options); + + /// + /// Generates the document from compilations that have already been loaded. + /// + /// What was loaded from the target. + /// The solution or project the compilations came from. + /// The options shaping the generated document. + /// The . + /// + /// Kept apart from loading so that generating can be exercised against a compilation built from source. Loading + /// one from disk needs an MSBuild workspace, and standing that up is neither quick nor reliable enough to put + /// in front of the only check that the generator and the compiler it is built against still agree. + /// + internal static GeneratedScreenplay GenerateFrom(LoadedCompilation loaded, string targetPath, ScreenplayGenerationOptions options) { - var loaded = await ScreenplayCompilationLoader.Load(targetPath, cancellationToken); if (loaded.Compilations.Count == 0) { return new GeneratedScreenplay(string.Empty, loaded.Diagnostics); diff --git a/Source/Cli/Commands/Screenplay/ScreenplayDiagnosticsWriter.cs b/Source/Cli/Commands/Screenplay/ScreenplayDiagnosticsWriter.cs index 9952734..c331f27 100644 --- a/Source/Cli/Commands/Screenplay/ScreenplayDiagnosticsWriter.cs +++ b/Source/Cli/Commands/Screenplay/ScreenplayDiagnosticsWriter.cs @@ -64,8 +64,8 @@ public static void Write(string format, IEnumerable diagno /// The diagnostic to write. /// The line. /// - /// The code and the location are both left out when they are absent — the compiler behind - /// screenplay validate assigns no codes, and a diagnostic about a whole document has no location. + /// The code and the location are both left out when they are absent — a diagnostic about a whole document has + /// no location, and not every reporting system assigns codes. /// public static string LineFor(ScreenplayDiagnostic diagnostic) { diff --git a/Source/Cli/Commands/Screenplay/ScreenplayValidation.cs b/Source/Cli/Commands/Screenplay/ScreenplayValidation.cs index 3390222..8be5eb2 100644 --- a/Source/Cli/Commands/Screenplay/ScreenplayValidation.cs +++ b/Source/Cli/Commands/Screenplay/ScreenplayValidation.cs @@ -46,13 +46,14 @@ public ValidatedScreenplay Validate(string targetPath) /// The diagnostic the compiler reported. /// The . /// - /// The compiler does not assign codes, so the code is left empty. The location carries the file and the position - /// within it, in the file(line,column) form editors and build logs already understand. + /// The compiler assigns every diagnostic a stable PLAY code, which is carried through so that a + /// diagnostic can be looked up, suppressed or matched on rather than only read. The location carries the file + /// and the position within it, in the file(line,column) form editors and build logs already understand. /// static ScreenplayDiagnostic Map(PlayFile file, Diagnostic diagnostic) => new( (ScreenplayDiagnosticSeverity)(int)diagnostic.Severity, - string.Empty, + diagnostic.Code, diagnostic.Message, $"{file.RelativePath}({diagnostic.Location.Line},{diagnostic.Location.Column})");