Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
</PropertyGroup>
<ItemGroup>
<!-- Cratis -->
<PackageVersion Include="Cratis.Arc.Screenplay" Version="21.0.1" />
<PackageVersion Include="Cratis.Chronicle.Connections" Version="16.19.1" />
<PackageVersion Include="Cratis.Chronicle.Contracts" Version="16.19.1" />
<PackageVersion Include="Cratis.Arc.Screenplay" Version="21.2.0" />
<PackageVersion Include="Cratis.Chronicle.Connections" Version="16.19.3" />
<PackageVersion Include="Cratis.Chronicle.Contracts" Version="16.19.3" />
<PackageVersion Include="Cratis.Fundamentals" Version="7.17.1" />
<!-- Prologue -->
<PackageVersion Include="Cratis.Prologue.Configuration" Version="1.1.0" />
<PackageVersion Include="Cratis.Prologue.Contracts" Version="1.1.0" />
<PackageVersion Include="Cratis.Prologue.Interpretation" Version="1.1.0" />
<PackageVersion Include="Cratis.Prologue.Interpreter.Contracts" Version="1.1.0" />
<PackageVersion Include="Cratis.Prologue.Screenplay" Version="1.1.0" />
<PackageVersion Include="Cratis.Screenplay" Version="1.6.4" />
<PackageVersion Include="Cratis.Prologue.Configuration" Version="1.2.0" />
<PackageVersion Include="Cratis.Prologue.Contracts" Version="1.2.0" />
<PackageVersion Include="Cratis.Prologue.Interpretation" Version="1.2.0" />
<PackageVersion Include="Cratis.Prologue.Interpreter.Contracts" Version="1.2.0" />
<PackageVersion Include="Cratis.Prologue.Screenplay" Version="1.2.0" />
<PackageVersion Include="Cratis.Screenplay" Version="2.1.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.10" />
<!-- CLI -->
<PackageVersion Include="MongoDB.Driver" Version="3.10.0" />
Expand Down Expand Up @@ -47,7 +47,7 @@
<!-- Testing -->
<PackageVersion Include="Cratis.Specifications" Version="4.0.0" />
<PackageVersion Include="Cratis.Specifications.XUnit" Version="4.0.0" />
<PackageVersion Include="Cratis.Chronicle.XUnit.Integration" Version="16.19.1" />
<PackageVersion Include="Cratis.Chronicle.XUnit.Integration" Version="16.19.3" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
Expand Down
6 changes: 3 additions & 3 deletions Documentation/reference/screenplay.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Type> <Name>'
error PLAY0027: [MyApp.play(5,5)] Invalid slice declaration 'slice Reserving' - expected 'slice <Type> <Name>'

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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Builds an application from source, so that generating from it runs the real generator rather than a substitute.
/// </summary>
/// <remarks>
/// Everything the CLI knows about the <c>Cratis.Arc.Screenplay</c> generator and the <c>Cratis.Screenplay</c>
/// compiler it holds through package references, and a mismatched pair of the two compiles clean and specs green
/// and then throws <see cref="MissingMethodException"/> 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.
/// <para>
/// 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.
/// </para>
/// </remarks>
public class an_application_built_from_source : Specification
{
/// <summary>
/// The name the compilation, and therefore the project the document is generated from, goes by.
/// </summary>
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";

/// <summary>
/// Gets what loading the application would have produced.
/// </summary>
protected LoadedCompilation Loaded { get; private set; }

void Establish() => Loaded = new(
[CSharpCompilation.Create(
ProjectName,
[CSharpSyntaxTree.ParseText(Source)],
References(),
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))],
[ProjectName],
[]);

/// <summary>
/// Gets the references the source needs to compile.
/// </summary>
/// <returns>The <see cref="MetadataReference"/> for every assembly this process was started with.</returns>
/// <remarks>
/// A compilation without references resolves nothing, not even <see cref="object"/>, 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.
/// </remarks>
static IEnumerable<MetadataReference> References() =>
((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!)
.Split(Path.PathSeparator)
.Select(assembly => MetadataReference.CreateFromFile(assembly));
}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
18 changes: 16 additions & 2 deletions Source/Cli/Commands/Screenplay/ArcScreenplayGeneration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,23 @@ namespace Cratis.Cli.Commands.Screenplay;
public sealed class ArcScreenplayGeneration : IScreenplayGeneration
{
/// <inheritdoc/>
public async Task<GeneratedScreenplay> Generate(string targetPath, ScreenplayGenerationOptions options, CancellationToken cancellationToken)
public async Task<GeneratedScreenplay> Generate(string targetPath, ScreenplayGenerationOptions options, CancellationToken cancellationToken) =>
GenerateFrom(await ScreenplayCompilationLoader.Load(targetPath, cancellationToken), targetPath, options);

/// <summary>
/// Generates the document from compilations that have already been loaded.
/// </summary>
/// <param name="loaded">What was loaded from the target.</param>
/// <param name="targetPath">The solution or project the compilations came from.</param>
/// <param name="options">The options shaping the generated document.</param>
/// <returns>The <see cref="GeneratedScreenplay"/>.</returns>
/// <remarks>
/// 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.
/// </remarks>
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);
Expand Down
4 changes: 2 additions & 2 deletions Source/Cli/Commands/Screenplay/ScreenplayDiagnosticsWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ public static void Write(string format, IEnumerable<ScreenplayDiagnostic> diagno
/// <param name="diagnostic">The diagnostic to write.</param>
/// <returns>The line.</returns>
/// <remarks>
/// The code and the location are both left out when they are absent — the compiler behind
/// <c>screenplay validate</c> 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.
/// </remarks>
public static string LineFor(ScreenplayDiagnostic diagnostic)
{
Expand Down
7 changes: 4 additions & 3 deletions Source/Cli/Commands/Screenplay/ScreenplayValidation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,14 @@ public ValidatedScreenplay Validate(string targetPath)
/// <param name="diagnostic">The diagnostic the compiler reported.</param>
/// <returns>The <see cref="ScreenplayDiagnostic"/>.</returns>
/// <remarks>
/// The compiler does not assign codes, so the code is left empty. The location carries the file and the position
/// within it, in the <c>file(line,column)</c> form editors and build logs already understand.
/// The compiler assigns every diagnostic a stable <c>PLAY</c> 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 <c>file(line,column)</c> form editors and build logs already understand.
/// </remarks>
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})");

Expand Down
Loading