Skip to content

Commit 6464f85

Browse files
committed
no-mistakes(review): Warn on skipped docs generator only when selected; fix pin wording
1 parent 1f0b3fc commit 6464f85

10 files changed

Lines changed: 113 additions & 32 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,9 @@ generator catalog and the Atlas evaluation are 1.2 work.
160160
packaged generators emit for (`GeneratorApplicability.Packaged`): an owned `routes` whose
161161
`AppliesTo` you changed is documented as the packaged one behaves, unless you edit the
162162
owned builder.
163-
`dotnet meta gen` skips a docs generator and says so, and `verify --codegen` leaves it out.
163+
`dotnet meta gen` and `verify --codegen` leave a docs generator out. `gen` says so only
164+
when a `--generators` selection names `api-docs`; an owned `Program.cs` that lists it runs
165+
without a warning.
164166
New public API in `MetaObjects.Codegen`: `IDocsGenerator`, `DocsRunFacts`,
165167
`GeneratorApplicability`, `GenContext.Applicability` and `GenContext.Docs`,
166168
`CodegenCli.RunDocs`, and `ValueObjectNames.ResolveValueObject` (moved from

‎docs/features/own-your-codegen.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,9 +1121,10 @@ packaged one behaves. To document your own surface, edit the owned builder
11211121
class, which is why the copy depends on nothing but the engine.
11221122
11231123
`Program.cs` lists `new ApiDocsGenerator()` beside the code generators. `dotnet meta gen` and
1124-
`verify --codegen` skip it, and `gen` says so (`"api-docs" writes the api reference, so
1125-
gen skipped it. Run it with dotnet meta docs.`): it writes pages under the docs directory,
1126-
not code under `--out`.
1124+
`verify --codegen` skip it: it writes pages under the docs directory, not code under `--out`.
1125+
The skip is silent while the generator is only in that list. When a `--generators` selection
1126+
names `api-docs`, `gen` says so (`"api-docs" writes the api reference, so gen skipped it.
1127+
Run it with dotnet meta docs.`), with or without an owned project.
11271128
11281129
### Ejecting routes hands over the helper runtime too
11291130

‎server/csharp/MetaObjects.Cli.Tests/EjectCommandTests.cs‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,37 @@ public void Ejecting_into_a_project_pinned_to_an_older_package_warns_and_names_t
158158
Assert.Contains("MetaObjects.Codegen 0.9.0", warning);
159159
Assert.Contains(EjectCommand.ToolVersion(), warning);
160160
Assert.Contains("will not compile", warning);
161-
Assert.Contains($"line {pinLine} of codegen/Codegen.csproj", warning);
161+
Assert.Contains($"set the version on line {pinLine} of codegen/Codegen.csproj to {EjectCommand.ToolVersion()}", warning);
162162
Assert.Contains(
163163
$"<PackageReference Include=\"MetaObjects.Codegen\" Version=\"{EjectCommand.ToolVersion()}\" />",
164164
result.Out[result.Out.ToList().IndexOf(warning) + 1]);
165165
Assert.Equal(csproj, File.ReadAllText(CsprojPath));
166166
}
167167

168+
// The line the warning names carries only the version in these shapes, so pasting a whole
169+
// PackageReference element over it would nest one inside another, or drop what else the
170+
// reference declares. The warning names the line and the version and prints no element.
171+
[Theory]
172+
[InlineData("<PackageReference Include=\"MetaObjects.Codegen\"\n Version=\"0.9.0\" />")]
173+
[InlineData("<PackageReference Include=\"MetaObjects.Codegen\">\n <Version>0.9.0</Version>\n </PackageReference>")]
174+
[InlineData("<PackageReference Include=\"MetaObjects.Codegen\" Version=\"0.9.0\" PrivateAssets=\"all\" />")]
175+
public void An_older_pin_not_in_the_scaffolded_form_is_named_by_line_and_version_only(string reference)
176+
{
177+
ScaffoldPinnedAt(null);
178+
var csproj =
179+
"<Project Sdk=\"Microsoft.NET.Sdk\">\n <ItemGroup>\n " + reference + "\n </ItemGroup>\n</Project>\n";
180+
File.WriteAllText(CsprojPath, csproj);
181+
var versionLine = Array.FindIndex(csproj.Split('\n'), l => l.Contains("0.9.0", StringComparison.Ordinal)) + 1;
182+
183+
var result = EjectCommand.Run(["api-docs"], _root, force: false);
184+
185+
Assert.Equal(0, result.ExitCode);
186+
var warning = Assert.Single(result.Out, l => l.StartsWith("warning:", StringComparison.Ordinal));
187+
Assert.Contains($"set the version on line {versionLine} of codegen/Codegen.csproj to {EjectCommand.ToolVersion()}.", warning);
188+
Assert.DoesNotContain(result.Out, l => l.Contains("<PackageReference", StringComparison.Ordinal));
189+
Assert.Equal(csproj, File.ReadAllText(CsprojPath));
190+
}
191+
168192
[Theory]
169193
[InlineData(null)] // the pin this tool wrote: equal
170194
[InlineData("99.0.0")] // newer

‎server/csharp/MetaObjects.Cli.Tests/EjectEndToEndTests.cs‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ private void PointScaffoldAtThisCheckout(string repoRoot)
198198
/// An owned api-docs generator, run for real (ADR-0025 / ADR-0034 Amendment 3): eject `routes` and
199199
/// `api-docs`, add a page to the owned api-docs copy, and run the owned project's `docs` command. The
200200
/// reference is written by the OWNED api-docs copy, so the added page is on disk beside the model's;
201-
/// and the same owned project's `gen` skips the docs generator and says so.
201+
/// and the same owned project's `gen` skips the docs generator, saying so only when asked for it by name.
202202
/// </summary>
203203
[Fact]
204204
public void Owned_api_docs_writes_the_reference()
@@ -241,12 +241,20 @@ public void Owned_api_docs_writes_the_reference()
241241
"written by the owned api-docs",
242242
File.ReadAllText(Directory.GetFiles(docsOut, "OWNED.md", SearchOption.AllDirectories).Single()));
243243

244-
// The same owned project's gen leaves the docs generator out and says why.
245-
var (genExit, genOutput) = RunCodegenProject(
246-
"gen", MetadataDir, "--out", OutDir, "--namespace", "Acme.Generated", "--generators", "api-docs");
244+
// The same owned project's gen leaves the docs generator out. With the generator only in the
245+
// owned list, that is the everyday run and it has nothing to say about it.
246+
var (genExit, genOutput) = RunCodegenProject("gen", MetadataDir, "--out", OutDir, "--namespace", "Acme.Generated");
247247
Assert.True(genExit == 0, $"gen failed (exit {genExit}):\n{genOutput}");
248-
Assert.Contains("writes the api reference, so `gen` skipped it", genOutput);
249-
Assert.Empty(Directory.Exists(OutDir) ? Directory.GetFiles(OutDir, "*.md", SearchOption.AllDirectories) : []);
248+
Assert.DoesNotContain("writes the api reference", genOutput);
249+
Assert.NotEmpty(Directory.GetFiles(OutDir, "*.cs", SearchOption.AllDirectories));
250+
Assert.Empty(Directory.GetFiles(OutDir, "*.md", SearchOption.AllDirectories));
251+
252+
// Asked for by name, it is still left out, and gen says why.
253+
var (namedExit, namedOutput) = RunCodegenProject(
254+
"gen", MetadataDir, "--out", OutDir, "--namespace", "Acme.Generated", "--generators", "api-docs");
255+
Assert.True(namedExit == 0, $"gen failed (exit {namedExit}):\n{namedOutput}");
256+
Assert.Contains("writes the api reference, so `gen` skipped it", namedOutput);
257+
Assert.Empty(Directory.GetFiles(OutDir, "*.md", SearchOption.AllDirectories));
250258
}
251259

252260
/// <summary>

‎server/csharp/MetaObjects.Cli.Tests/GenListAndSelectionTests.cs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,21 @@ public void Previously_omitted_output_prompt_is_now_selectable_and_emits()
9999
Assert.True(File.Exists(Path.Combine(OutDir, "Alpha.responseFormat.cs")));
100100
}
101101

102+
[Fact]
103+
public void Selecting_the_docs_generator_writes_no_pages_and_says_which_command_does()
104+
{
105+
// api-docs is in the catalog, so a selection can name it; its pages belong to `dotnet meta docs`.
106+
var outcome = GenCommand.Run(
107+
MetaDir, OutDir, "Acme.Generated",
108+
emitAbstractShapes: false, generatorNames: ["entity", "api-docs"], templateRoot: null);
109+
110+
Assert.True(outcome.Ok, string.Join("; ", outcome.LoadErrors));
111+
var warning = Assert.Single(outcome.Result!.Warnings, w => w.Contains("\"api-docs\"", StringComparison.Ordinal));
112+
Assert.Contains("`dotnet meta docs`", warning);
113+
Assert.NotEmpty(Directory.GetFiles(OutDir, "*.cs", SearchOption.AllDirectories));
114+
Assert.Empty(Directory.GetFiles(OutDir, "*.md", SearchOption.AllDirectories));
115+
}
116+
102117
[Fact]
103118
public void Unknown_generator_name_surfaces_as_an_error_not_a_throw()
104119
{

‎server/csharp/MetaObjects.Cli/EjectCommand.cs‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,9 @@ public static bool IsOlderVersion(string pinned, string required)
134134

135135
/// <summary>The version <paramref name="csprojPath"/> pins <c>MetaObjects.Codegen</c> to and the
136136
/// 1-based line that carries it, or <c>null</c> when the file is not readable XML or has no such
137-
/// PackageReference with a version.</summary>
138-
private static (string Version, int Line)? PinnedCodegenVersion(string csprojPath)
137+
/// PackageReference with a version. <c>Scaffolded</c> is true when that line is the whole
138+
/// reference in the form eject scaffolds: one element, <c>Include</c> and <c>Version</c>, no children.</summary>
139+
private static (string Version, int Line, bool Scaffolded)? PinnedCodegenVersion(string csprojPath)
139140
{
140141
XDocument doc;
141142
try { doc = XDocument.Load(csprojPath, LoadOptions.SetLineInfo); }
@@ -148,8 +149,10 @@ private static (string Version, int Line)? PinnedCodegenVersion(string csprojPat
148149
?? reference?.Elements().FirstOrDefault(e => e.Name.LocalName == "Version");
149150
return version switch
150151
{
151-
XAttribute a => (a.Value.Trim(), ((IXmlLineInfo)a).LineNumber),
152-
XElement e => (e.Value.Trim(), ((IXmlLineInfo)e).LineNumber),
152+
XAttribute a => (a.Value.Trim(), ((IXmlLineInfo)a).LineNumber,
153+
((IXmlLineInfo)a).LineNumber == ((IXmlLineInfo)reference!).LineNumber
154+
&& reference.Attributes().Count() == 2 && !reference.Nodes().Any()),
155+
XElement e => (e.Value.Trim(), ((IXmlLineInfo)e).LineNumber, false),
153156
_ => null,
154157
};
155158
}
@@ -160,15 +163,14 @@ private static IReadOnlyList<string> OlderPinWarning(string csprojPath)
160163
{
161164
var tool = ToolVersion();
162165
if (PinnedCodegenVersion(csprojPath) is not { } pin || !IsOlderVersion(pin.Version, tool)) return [];
163-
return
164-
[
165-
"",
166+
var warning =
166167
$"warning: codegen/Codegen.csproj pins {PinnedPackage} {pin.Version}, older than the {tool} this " +
167168
$"eject wrote its sources from. Those sources are written against the {tool} package API, so the " +
168169
$"owned project will not compile until the pin is {tool} or later. Eject never edits that file: " +
169-
$"change line {pin.Line} of codegen/Codegen.csproj to",
170-
$" <PackageReference Include=\"{PinnedPackage}\" Version=\"{tool}\" />",
171-
];
170+
$"set the version on line {pin.Line} of codegen/Codegen.csproj to {tool}";
171+
return pin.Scaffolded
172+
? ["", warning + ", so the line reads", $" <PackageReference Include=\"{PinnedPackage}\" Version=\"{tool}\" />"]
173+
: ["", warning + "."];
172174
}
173175

174176
private static string ProgramCsContent(IEnumerable<string> classNames)

‎server/csharp/MetaObjects.Cli/GenCommand.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ public static Outcome Run(
169169
// `codegen/Program.cs` calls too, with its owned generators composed into the same
170170
// selection (CodegenCli.ComposeGenerators) — one implementation, not two.
171171
var apiOutcome = MetaObjects.Codegen.CodegenCli.RunGen(
172-
load, outDir, ns, emitAbstractShapes, generators, projectRoot, columnNaming, baseline);
172+
load, outDir, ns, emitAbstractShapes, generators, projectRoot, columnNaming, baseline, names);
173173
return new Outcome(apiOutcome.LoadErrors, apiOutcome.Result);
174174
}
175175

‎server/csharp/MetaObjects.Codegen.Tests/ApiDocsRunTests.cs‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// The docs run: `api-docs` as a generator (ADR-0025), what runs it, and what it documents.
22
//
33
// A project can own the api reference like any other generator. These cover the engine around it:
4-
// a docs generator is not a code generator (`gen` skips it and says so), the docs run takes a
4+
// a docs generator is not a code generator (`gen` skips it, and says so when asked for it by name), the docs run takes a
55
// project's own copy in place of the packaged one, and what the reference documents is what the
66
// PACKAGED generators emit for, whichever generators the run names.
77

@@ -94,9 +94,11 @@ public void Two_pages_at_one_path_fail_the_run_before_anything_is_written()
9494
}
9595

9696
[Fact]
97-
public void A_code_run_skips_a_docs_generator_and_says_so()
97+
public void A_code_run_whose_selection_names_a_docs_generator_skips_it_and_says_so()
9898
{
99-
var outcome = CodegenCli.RunGen(Load(), _out, "Acme.Generated", false, [new ApiDocsGenerator(), new EntityGenerator()]);
99+
var outcome = CodegenCli.RunGen(
100+
Load(), _out, "Acme.Generated", false, [new ApiDocsGenerator(), new EntityGenerator()],
101+
selection: ["api-docs", "entity"]);
100102

101103
Assert.True(outcome.Ok);
102104
Assert.Contains(outcome.Result!.Warnings, w => w == CodegenCli.DocsGeneratorSkipped("api-docs"));
@@ -105,6 +107,24 @@ public void A_code_run_skips_a_docs_generator_and_says_so()
105107
Assert.NotEmpty(outcome.Result.Files);
106108
}
107109

110+
// An owned project lists its docs generator beside its code generators, because that one list is
111+
// where `docs` finds it. A code run that did not ask for it by name skips it and has nothing to say:
112+
// a warning here would fire on every `gen` and could only be cleared by giving the generator up.
113+
[Theory]
114+
[InlineData(null)]
115+
[InlineData("entity")]
116+
public void A_code_run_that_only_finds_a_docs_generator_in_its_list_skips_it_silently(string? selected)
117+
{
118+
var outcome = CodegenCli.RunGen(
119+
Load(), _out, "Acme.Generated", false, [new ApiDocsGenerator(), new EntityGenerator()],
120+
selection: selected is null ? null : [selected]);
121+
122+
Assert.True(outcome.Ok);
123+
Assert.DoesNotContain(outcome.Result!.Warnings, w => w.Contains("api reference", StringComparison.Ordinal));
124+
Assert.DoesNotContain(outcome.Result.Files, f => f.Path.Contains("AGENT-API", StringComparison.Ordinal));
125+
Assert.NotEmpty(outcome.Result.Files);
126+
}
127+
108128
[Fact]
109129
public void A_drift_check_leaves_a_docs_generator_out_too()
110130
{

‎server/csharp/MetaObjects.Codegen/CodegenCli.cs‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,17 @@ public static GenOutcome RunGen(
200200
/// to the current directory for a caller that names no project (mirrors
201201
/// <c>GenConfig.ProjectRoot</c>'s own default).
202202
/// </param>
203+
/// <param name="selection">
204+
/// The <c>--generators</c> names the run was asked for, when it was asked by name. A docs
205+
/// generator is never run here; one this selection names is reported as skipped
206+
/// (<see cref="DocsGeneratorSkipped"/>), and one that is only in <paramref name="generators"/>
207+
/// — an owned list, where the docs run looks for it — is left out without a word.
208+
/// </param>
203209
public static GenOutcome RunGen(
204210
LoadResult load, string outDir, string ns, bool emitAbstractShapes,
205211
IReadOnlyList<IGenerator> generators, string? projectRoot = null,
206-
ColumnNamingStrategy columnNaming = ColumnNamingStrategy.Literal, string baseline = "default")
212+
ColumnNamingStrategy columnNaming = ColumnNamingStrategy.Literal, string baseline = "default",
213+
IReadOnlyList<string>? selection = null)
207214
{
208215
var loadErrors = load.Errors.Select(e => e.Code.ToString()).ToList();
209216
if (loadErrors.Count > 0) return new GenOutcome(loadErrors, null);
@@ -228,9 +235,10 @@ public static GenOutcome RunGen(
228235
IncludeNames = generators.Any(g => g.Name == "names"),
229236
};
230237

231-
// A docs generator writes the api reference, not code: CodegenRunner leaves it out, and
232-
// this run says so rather than writing pages into the generated-code tree.
233-
var docsSkipped = generators.OfType<IDocsGenerator>().Select(g => g.Name).ToList();
238+
// A docs generator writes the api reference, not code: CodegenRunner leaves it out. A run
239+
// asked for one by name says so; an owned list carries one for the docs run, not for this.
240+
var docsSkipped = generators.OfType<IDocsGenerator>().Select(g => g.Name)
241+
.Where(name => selection?.Contains(name) == true).ToList();
234242

235243
CodegenRunner.RunResult result;
236244
try
@@ -253,7 +261,7 @@ public static GenOutcome RunGen(
253261
return new GenOutcome(loadErrors, result);
254262
}
255263

256-
/// <summary>What a code run says about a docs generator it was handed and did not run.</summary>
264+
/// <summary>What a code run says about a docs generator its selection named and it did not run.</summary>
257265
public static string DocsGeneratorSkipped(string name) =>
258266
$"\"{name}\" writes the api reference, so `gen` skipped it. Run it with `dotnet meta docs`.";
259267

@@ -488,7 +496,7 @@ private static int RunGenArgs(string[] rest, IReadOnlyList<IGenerator> owned)
488496
}
489497

490498
WarnUnknownAttrs(meta);
491-
var outcome = RunGen(meta.Load(), a.OutDir, a.Namespace, a.EmitAbstractShapes, suite, projectRoot, columnNaming, a.Baseline);
499+
var outcome = RunGen(meta.Load(), a.OutDir, a.Namespace, a.EmitAbstractShapes, suite, projectRoot, columnNaming, a.Baseline, a.Selection);
492500
if (!outcome.Ok)
493501
{
494502
ReportFailure(outcome, "codegen (owned) gen");

‎server/csharp/MetaObjects.Codegen/DocsGenerator.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
// from generators, but a docs generator must never be run by `gen`: it writes pages under the
55
// docs directory, not code under the generated-code directory, and `gen`'s hash manifest and
66
// overwrite policy are about code. A generator that takes part in the docs run says so by
7-
// implementing IDocsGenerator; `gen` and `verify --codegen` skip it and say they did.
7+
// implementing IDocsGenerator; `gen` and `verify --codegen` skip it, and `gen` says so when its
8+
// `--generators` selection named it.
89
//
910
// One list serves both commands. An owned `codegen/Program.cs` lists a docs generator beside the
1011
// code generators, and each command runs the half that is its own.

0 commit comments

Comments
 (0)