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
6 changes: 3 additions & 3 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ solution := root_folder / "SourceGeneratorFramework.slnx"
build_configuration := "Release"
artifacts_folder := "./artifacts"
default_test_filter := "/*/*/*/*/"
pipeline_version := "0.2.1"

pipeline_feed := "https://api.nuget.org/v3/index.json"
pipeline_tool := ".tools/purview-build/purview-build"

Expand All @@ -19,7 +19,7 @@ default:
[private]
ensure-pipeline-tool:
if [ ! -x "{{ pipeline_tool }}" ]; then \
dotnet tool install Purview.Build --tool-path .tools/purview-build --add-source "{{ pipeline_feed }}" --version "{{ pipeline_version }}"; \
dotnet tool install Purview.Build --tool-path .tools/purview-build --add-source "{{ pipeline_feed }}"; \
fi

# Run the PR pipeline (restore, build, lint, tests)
Expand All @@ -46,7 +46,7 @@ pipeline-release *args:
# Run the release pipeline (restore, build, lint, tests, pack, local nuget publish)
# Note: `just` runs recipes through the shell, which strips backslashes from unquoted arguments.
# Use the LOCAL_NUGET_FEED_PATH environment variable or forward slashes, e.g.
# just pipeline-local-release --PublishLocalNuGet:LocalFeedPath=p:/_sync-projects/.local-nuget/
# just pipeline-local-release --PublishLocalNuGet:LocalFeedPath=p:/_sync-projects/.local-nuget/
[group('Pipeline')]
pipeline-local-release *args:
just ensure-pipeline-tool
Expand Down
28 changes: 28 additions & 0 deletions docs/code-writer.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,34 @@ writer.MethodCall("Create", ["x"], receiver: "factory", genericArguments: [TypeR
// factory.Create<string>(x);
```

A **chained** invocation — where the result of each call is the receiver of the next, and a postfix is
applied to the final result — is expressed with `MethodCallChain`/`AwaitedMethodCallChain`. The chain
is written as an expression (no terminating semicolon), so it composes as the value of an
`Assignment`/`Return` expression callback:

```csharp
writer.Assignment(
"var hostKitOptions",
expression => expression.MethodCallChain(
"builder.Configuration.GetSection",
[$"{name}.SectionName"],
chain => chain.Method("Get", genericArguments: [optionsType]).Postfix(" ?? new()")));
// var hostKitOptions = builder.Configuration.GetSection("x.SectionName").Get<Options>() ?? new();
```

- `rootMethod` may include the receiver (e.g. `builder.Configuration.GetSection`); each subsequent
`.Method(...)` call implicitly uses the previous result as its receiver.
- `genericArguments` provides the `<...>` type arguments for a segment.
- `Postfix(expression)` appends a trailing expression such as `?? new()` or `!`.

A null-conditional receiver — `onBuilt?.Invoke(this, builder);` — is written with the `nullConditional`
argument on the structured `MethodCallOn`/`AwaitedMethodCallOn` overloads:

```csharp
writer.MethodCallOn("onBuilt", "Invoke", ["this", "builder"], nullConditional: true);
// onBuilt?.Invoke(this, builder);
```

### Conditional statements

`IfBlock`/`IfBlockScope` write an `if` block. `ElseIf`/`ElseIfScope` chain an `else if` block after an
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "purview-sourcegeneratorframework",
"version": "1.0.0-prerelease.34",
"version": "1.0.0-prerelease.35",
"private": true
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,36 @@ options with
.ElseIf("value == 0", branch => branch.Return("\"zero\""))
.Else(branch => branch.Return("\"positive\""))
);

body.Method(
"Configure",
TypeIdentity.Create<string>().AsTypeReference(),
TypeDeclarationAccessibility.Public,
options =>
options with
{
IsStatic = true,
Parameters =
[
new("source", TypeIdentity.Create<string>().AsTypeReference()),
new("onBuilt", PurviewTypeLibrary.System.Action.AsTypeReference()),
],
},
methodBody =>
{
methodBody.Assignment(
"var hostKitOptions",
expression =>
expression.MethodCallChain(
"source.Trim",
[],
chain => chain.Method("ToUpper").Postfix(" ?? string.Empty")
)
);
methodBody.MethodCallOn("onBuilt", "Invoke", [], nullConditional: true);
methodBody.Return("hostKitOptions");
}
);
}
);

Expand Down
191 changes: 183 additions & 8 deletions src/src/SourceGeneratorShared/CodeWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2128,7 +2128,7 @@ public CodeWriter MultiLineParameters(params string[] parameters)
/// <returns>The current writer.</returns>
/// <example><code>writer.MethodCall("Run", "value", "cancellationToken"); // Run(value, cancellationToken);</code></example>
public CodeWriter MethodCall(string methodName, params string[] arguments) =>
MethodCallCore(methodName, arguments, receiver: null, genericArguments: null, false, false);
MethodCallCore(methodName, arguments, receiver: null, genericArguments: null, false, false, false);

/// <summary>
/// Writes an awaited method invocation statement.
Expand All @@ -2138,7 +2138,7 @@ public CodeWriter MethodCall(string methodName, params string[] arguments) =>
/// <returns>The current writer.</returns>
/// <example><code>writer.AwaitedMethodCall("LoadAsync", "cancellationToken"); // await LoadAsync(cancellationToken);</code></example>
public CodeWriter AwaitedMethodCall(string methodName, params string[] arguments) =>
MethodCallCore(methodName, arguments, receiver: null, genericArguments: null, false, true);
MethodCallCore(methodName, arguments, receiver: null, genericArguments: null, false, true, false);

/// <summary>
/// Writes a method invocation on a receiver, such as <c>variable.Method(arg)</c>.
Expand All @@ -2149,7 +2149,35 @@ public CodeWriter AwaitedMethodCall(string methodName, params string[] arguments
/// <returns>The current writer.</returns>
/// <example><code>writer.MethodCallOn("service", "Add", "value"); // service.Add(value);</code></example>
public CodeWriter MethodCallOn(string receiver, string methodName, params string[] arguments) =>
MethodCallCore(methodName, arguments, receiver, genericArguments: null, false, false);
MethodCallCore(methodName, arguments, receiver, genericArguments: null, false, false, false);

/// <summary>
/// Writes a method invocation on a receiver from structured argument declarations.
/// </summary>
/// <param name="receiver">The receiver expression written before the method name.</param>
/// <param name="methodName">The method name.</param>
/// <param name="arguments">The structured arguments to invoke the method with.</param>
/// <param name="nullConditional">
/// Whether the receiver is invoked with the null-conditional operator (<c>?.</c>) so the call is
/// skipped when the receiver is <see langword="null"/>.
/// </param>
/// <returns>The current writer.</returns>
/// <example><code>writer.MethodCallOn("onBuilt", "Invoke", ["this", "builder"], nullConditional: true); // onBuilt?.Invoke(this, builder);</code></example>
public CodeWriter MethodCallOn(
string receiver,
string methodName,
IEnumerable<MethodCallArgumentOptions> arguments,
bool nullConditional = false
) =>
MethodCallCore(
methodName,
(arguments ?? throw new ArgumentNullException(nameof(arguments))).Select(RenderCallArgument),
receiver,
genericArguments: null,
false,
false,
nullConditional
);

/// <summary>
/// Writes an awaited method invocation on a receiver, such as <c>await variable.MethodAsync(arg)</c>.
Expand All @@ -2160,7 +2188,35 @@ public CodeWriter MethodCallOn(string receiver, string methodName, params string
/// <returns>The current writer.</returns>
/// <example><code>writer.AwaitedMethodCallOn("service", "LoadAsync", "token"); // await service.LoadAsync(token);</code></example>
public CodeWriter AwaitedMethodCallOn(string receiver, string methodName, params string[] arguments) =>
MethodCallCore(methodName, arguments, receiver, genericArguments: null, false, true);
MethodCallCore(methodName, arguments, receiver, genericArguments: null, false, true, false);

/// <summary>
/// Writes an awaited method invocation on a receiver from structured argument declarations.
/// </summary>
/// <param name="receiver">The receiver expression written before the method name.</param>
/// <param name="methodName">The method name.</param>
/// <param name="arguments">The structured arguments to invoke the method with.</param>
/// <param name="nullConditional">
/// Whether the receiver is invoked with the null-conditional operator (<c>?.</c>) so the call is
/// skipped when the receiver is <see langword="null"/>.
/// </param>
/// <returns>The current writer.</returns>
/// <example><code>writer.AwaitedMethodCallOn("service", "LoadAsync", ["token"], nullConditional: true); // await service?.LoadAsync(token);</code></example>
public CodeWriter AwaitedMethodCallOn(
string receiver,
string methodName,
IEnumerable<MethodCallArgumentOptions> arguments,
bool nullConditional = false
) =>
MethodCallCore(
methodName,
(arguments ?? throw new ArgumentNullException(nameof(arguments))).Select(RenderCallArgument),
receiver,
genericArguments: null,
false,
true,
nullConditional
);

/// <summary>
/// Writes a method invocation from structured argument declarations.
Expand Down Expand Up @@ -2217,7 +2273,8 @@ public CodeWriter AwaitedMethodCall(
receiver,
genericArguments,
writeArgumentsOnSeparateLines,
true
true,
false
);

/// <summary>
Expand All @@ -2236,15 +2293,16 @@ public CodeWriter MethodCall(
string? receiver = null,
IEnumerable<TypeReference>? genericArguments = null,
bool writeArgumentsOnSeparateLines = false
) => MethodCallCore(methodName, arguments, receiver, genericArguments, writeArgumentsOnSeparateLines, false);
) => MethodCallCore(methodName, arguments, receiver, genericArguments, writeArgumentsOnSeparateLines, false, false);

CodeWriter MethodCallCore(
string methodName,
IEnumerable<string?> arguments,
string? receiver,
IEnumerable<TypeReference>? genericArguments,
bool writeArgumentsOnSeparateLines,
bool isAwaited
bool isAwaited,
bool nullConditional
)
{
ValidateStatementPart(methodName, nameof(methodName));
Expand All @@ -2262,7 +2320,7 @@ bool isAwaited
if (isAwaited)
Write("await ");
if (receiver is not null)
Write(receiver).Write('.');
Write(receiver).Write(nullConditional ? "?." : ".");
Write(methodName);
if (genericArgumentList.Length > 0)
{
Expand Down Expand Up @@ -2332,6 +2390,123 @@ bool isAwaited
return true;
}

/// <summary>
/// Writes a chained method-call expression in which the result of each call is the receiver of the
/// next. The chain is written without a trailing semicolon so it composes as the value of an
/// <see cref="Assignment(string, Action{CodeWriter})"/>, <see cref="Return(Action{CodeWriter})"/>, or
/// other expression. A standalone chain statement is terminated by appending <c>.Line(";")</c>.
/// </summary>
/// <param name="rootMethod">The root invocation, optionally including a receiver, such as <c>builder.Configuration.GetSection</c>.</param>
/// <param name="arguments">The root argument expressions.</param>
/// <param name="configure">The callback that appends chained invocations and the postfix.</param>
/// <param name="genericArguments">Optional generic type arguments for the root invocation.</param>
/// <returns>The current writer.</returns>
/// <example><code>writer.Assignment("var value", value =&gt; value.MethodCallChain(
/// "builder.Configuration.GetSection", [$"{name}.SectionName"],
/// chain =&gt; chain.Method("Get", genericArguments: [optionsType]).Postfix("?? new()")));
/// // var value = builder.Configuration.GetSection("x.SectionName").Get&lt;Options&gt;() ?? new();</code></example>
public CodeWriter MethodCallChain(
string rootMethod,
IEnumerable<string?> arguments,
Action<MethodChainBuilder> configure,
IEnumerable<TypeReference>? genericArguments = null
) => MethodCallChainCore(rootMethod, arguments, configure, genericArguments, isAwaited: false);

/// <summary>
/// Writes an awaited chained method-call expression in which the result of each call is the receiver
/// of the next. The chain is written without a trailing semicolon so it composes as an expression.
/// </summary>
/// <param name="rootMethod">The root invocation, optionally including a receiver.</param>
/// <param name="arguments">The root argument expressions.</param>
/// <param name="configure">The callback that appends chained invocations and the postfix.</param>
/// <param name="genericArguments">Optional generic type arguments for the root invocation.</param>
/// <returns>The current writer.</returns>
/// <example><code>writer.Return(value =&gt; value.AwaitedMethodCallChain(
/// "service.LoadAsync", ["token"], chain =&gt; chain.Method("Configure")));</code></example>
public CodeWriter AwaitedMethodCallChain(
string rootMethod,
IEnumerable<string?> arguments,
Action<MethodChainBuilder> configure,
IEnumerable<TypeReference>? genericArguments = null
) => MethodCallChainCore(rootMethod, arguments, configure, genericArguments, isAwaited: true);

CodeWriter MethodCallChainCore(
string rootMethod,
IEnumerable<string?> arguments,
Action<MethodChainBuilder> configure,
IEnumerable<TypeReference>? genericArguments,
bool isAwaited
)
{
ValidateStatementPart(rootMethod, nameof(rootMethod));
if (arguments is null)
throw new ArgumentNullException(nameof(arguments));
if (configure is null)
throw new ArgumentNullException(nameof(configure));

var rootArguments = arguments.ToArray();
for (var index = 0; index < rootArguments.Length; index++)
ValidateStatementPart(rootArguments[index], nameof(arguments));

var builder = new MethodChainBuilder(rootMethod, rootArguments, genericArguments);
configure(builder);

return RenderMethodChain(builder, isAwaited);
}

CodeWriter RenderMethodChain(MethodChainBuilder builder, bool isAwaited)
{
if (isAwaited)
Write("await ");

Write(builder.RootMethod);
RenderGenericArgumentList(builder.RootGenericArguments);
RenderInvocationArguments(builder.RootArguments);

for (var index = 0; index < builder.Segments.Count; index++)
{
var segment = builder.Segments[index];
Write('.');
Write(segment.MethodName);
RenderGenericArgumentList(segment.GenericArguments);
RenderInvocationArguments(segment.Arguments);
}

if (builder.PostfixExpression is not null)
Write(builder.PostfixExpression);

return this;
}

void RenderGenericArgumentList(ImmutableArray<TypeReference> genericArguments)
{
if (genericArguments.IsDefaultOrEmpty)
return;

Write('<');
for (var index = 0; index < genericArguments.Length; index++)
{
if (index != 0)
Write(", ");
if (genericArguments[index].IsEmpty)
throw new ArgumentException("Generic arguments cannot be empty.");
TypeReference(genericArguments[index]);
}
Write('>');
}

void RenderInvocationArguments(ImmutableArray<string?> arguments)
{
Write('(');
for (var index = 0; index < arguments.Length; index++)
{
if (index != 0)
Write(", ");
Write(arguments[index]);
}
Write(')');
}

/// <summary>
/// Writes an assignment statement.
/// </summary>
Expand Down
Loading