Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace Bit.BlazorUI.SourceGenerators.AutoInject;

public static class AutoInjectHelper
internal static class AutoInjectHelper
{
public static readonly string AutoInjectAttributeFullName = "Microsoft.Extensions.DependencyInjection.AutoInjectAttribute"; //typeof(AutoInjectAttribute).FullName;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Bit.BlazorUI.SourceGenerators.AutoInject;

internal readonly record struct AutoInjectMember(string Name, string TypeDisplay, bool IsField, bool IsNullable);
Original file line number Diff line number Diff line change
Expand Up @@ -2,113 +2,94 @@
using System.Linq;
using System.Text;
using Bit.SourceGenerators;
using Microsoft.CodeAnalysis;

namespace Bit.BlazorUI.SourceGenerators.AutoInject;

public static class AutoInjectNormalClassHandler
internal static class AutoInjectNormalClassHandler
{
public static string? Generate(INamedTypeSymbol? attributeSymbol, INamedTypeSymbol? classSymbol, IReadOnlyCollection<ISymbol> eligibleMembers)
public static string? Generate(
string classNamespace,
string classNameForCode,
string className,
IReadOnlyCollection<AutoInjectMember> directMembers,
IReadOnlyCollection<AutoInjectMember> baseMembers)
{
if (classSymbol is null)
{
return null;
}

if (AutoInjectHelper.IsContainingSymbolEqualToContainingNamespace(classSymbol) is false)
{
return null;
}

string classNamespace = classSymbol.ContainingNamespace.ToDisplayString();

IReadOnlyCollection<ISymbol> baseEligibleMembers = AutoInjectHelper.GetBaseClassEligibleMembers(classSymbol, attributeSymbol);
IReadOnlyCollection<ISymbol> sortedMembers = eligibleMembers.OrderBy(o => o.Name).ToList();
var sortedMembers = directMembers.OrderBy(o => o.Name).ToList();

string source = $@"
namespace {classNamespace}
{{
public partial class {AutoInjectHelper.GenerateClassName(classSymbol)}
public partial class {classNameForCode}
{{
{GenerateConstructor(classSymbol, sortedMembers, baseEligibleMembers)}
{GenerateConstructor(className, sortedMembers, baseMembers)}
}}
}}";
return source;
}

private static string GenerateConstructor(INamedTypeSymbol classSymbol, IReadOnlyCollection<ISymbol> eligibleMembers, IReadOnlyCollection<ISymbol> baseEligibleMembers)
private static string GenerateConstructor(string className, IReadOnlyCollection<AutoInjectMember> directMembers, IReadOnlyCollection<AutoInjectMember> baseMembers)
{
string generateConstructor = $@"
[global::System.CodeDom.Compiler.GeneratedCode(""Bit.SourceGenerators"",""{BitSourceGeneratorUtil.GetPackageVersion()}"")]
[global::System.Diagnostics.DebuggerNonUserCode]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
{"\t\t"}public {classSymbol.Name}({GenerateConstructorParameters(eligibleMembers, baseEligibleMembers)}){PassParametersToBaseClass(baseEligibleMembers)}
{"\t\t"}public {className}({GenerateConstructorParameters(directMembers, baseMembers)}){PassParametersToBaseClass(baseMembers)}
{"\t\t"}{{
{AssignedInjectedParametersToMembers(eligibleMembers)}
{AssignMembersFromParameters(directMembers)}
{"\t\t"}}}
";
return generateConstructor;
}

private static string PassParametersToBaseClass(IReadOnlyCollection<ISymbol> baseEligibleMembers)
private static string PassParametersToBaseClass(IReadOnlyCollection<AutoInjectMember> baseMembers)
{
if (baseEligibleMembers.Any() is false)
if (baseMembers.Any() is false)
return string.Empty;

StringBuilder baseConstructor = new();

baseConstructor.Append(": base(");

foreach (ISymbol symbol in baseEligibleMembers)
foreach (var member in baseMembers)
{
baseConstructor.Append($@"{'\n'}{"\t\t\t\t\t\t"}autoInjected{AutoInjectHelper.FormatMemberName(symbol.Name)},");
baseConstructor.Append($@"{'\n'}{"\t\t\t\t\t\t"}autoInjected{AutoInjectHelper.FormatMemberName(member.Name)},");
}

baseConstructor.Length--;

baseConstructor.Append(')');

return baseConstructor.ToString();
}

private static string AssignedInjectedParametersToMembers(IReadOnlyCollection<ISymbol> eligibleMembers)
private static string AssignMembersFromParameters(IReadOnlyCollection<AutoInjectMember> directMembers)
{
StringBuilder stringBuilder = new();
foreach (ISymbol symbol in eligibleMembers)
foreach (var member in directMembers)
{
if (stringBuilder.Length > 0)
{
stringBuilder.Append('\n');
}
stringBuilder.Append("\t\t\t")
.Append($@"{symbol.Name} = autoInjected{AutoInjectHelper.FormatMemberName(symbol.Name)};");
.Append($@"{member.Name} = autoInjected{AutoInjectHelper.FormatMemberName(member.Name)};");
}

return stringBuilder.ToString();
}

private static string GenerateConstructorParameters(IReadOnlyCollection<ISymbol> eligibleMembers, IReadOnlyCollection<ISymbol> baseEligibleMembers)
private static string GenerateConstructorParameters(IReadOnlyCollection<AutoInjectMember> directMembers, IReadOnlyCollection<AutoInjectMember> baseMembers)
{
StringBuilder stringBuilder = new();
List<ISymbol> members = new(eligibleMembers.Count + baseEligibleMembers.Count);
var allMembers = directMembers.Concat(baseMembers).OrderBy(o => o.Name).ToList();

members.AddRange(eligibleMembers);
members.AddRange(baseEligibleMembers);
members = members.OrderBy(o => o.Name).ToList();

foreach (ISymbol member in members)
foreach (var member in allMembers)
{
if (member is IFieldSymbol fieldSymbol)
stringBuilder.Append(
$@"{'\n'}{"\t\t\t"}{fieldSymbol.Type} autoInjected{AutoInjectHelper.FormatMemberName(fieldSymbol.Name)},");

if (member is IPropertySymbol propertySymbol)
stringBuilder.Append(
$@"{'\n'}{"\t\t\t"}{propertySymbol.Type} autoInjected{AutoInjectHelper.FormatMemberName(propertySymbol.Name)},");
var nullValue = member.IsNullable ? " = null" : string.Empty;
stringBuilder.Append($@"{'\n'}{"}\t\t\t"}{member.TypeDisplay} autoInjected{AutoInjectHelper.FormatMemberName(member.Name)}{nullValue},");
Comment thread
msynk marked this conversation as resolved.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

stringBuilder.Length--;

return stringBuilder.ToString();
}
Comment thread
msynk marked this conversation as resolved.
}

Original file line number Diff line number Diff line change
@@ -1,67 +1,51 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Bit.SourceGenerators;
using Microsoft.CodeAnalysis;

namespace Bit.BlazorUI.SourceGenerators.AutoInject;

public static class AutoInjectRazorComponentHandler
internal static class AutoInjectRazorComponentHandler
{
public static string? Generate(INamedTypeSymbol? classSymbol, IReadOnlyCollection<ISymbol> eligibleMembers)
public static string? Generate(
string classNamespace,
string classNameForCode,
IReadOnlyCollection<AutoInjectMember> directMembers)
{
if (classSymbol is null)
{
return null;
}

if (AutoInjectHelper.IsContainingSymbolEqualToContainingNamespace(classSymbol) is false)
{
return null;
}

string classNamespace = classSymbol.ContainingNamespace.ToDisplayString();

IReadOnlyCollection<ISymbol> sortedMembers = eligibleMembers.OrderBy(o => o.Name).ToList();

string source = $@"
using Microsoft.AspNetCore.Components;
using System.ComponentModel;

namespace {classNamespace}
{{
public partial class {AutoInjectHelper.GenerateClassName(classSymbol)}
public partial class {classNameForCode}
{{
{GenerateInjectableProperties(sortedMembers)}
{GenerateInjectableProperties(directMembers)}
}}
}}";
return source;
}

private static string GenerateInjectableProperties(IReadOnlyCollection<ISymbol> eligibleMembers)
private static string GenerateInjectableProperties(IReadOnlyCollection<AutoInjectMember> members)
{
StringBuilder stringBuilder = new StringBuilder();

foreach (ISymbol member in eligibleMembers)
foreach (var member in members)
{
if (member is IFieldSymbol fieldSymbol)
stringBuilder.Append(GenerateProperty(fieldSymbol.Type, fieldSymbol.Name));

if (member is IPropertySymbol propertySymbol)
stringBuilder.Append(GenerateProperty(propertySymbol.Type, propertySymbol.Name));
stringBuilder.Append(GenerateProperty(member.TypeDisplay, member.Name));
}

return stringBuilder.ToString();
}

private static string GenerateProperty(ITypeSymbol @type, string name)
private static string GenerateProperty(string typeDisplay, string name)
{
return $@"
[global::System.CodeDom.Compiler.GeneratedCode(""Bit.SourceGenerators"",""{BitSourceGeneratorUtil.GetPackageVersion()}"")]
[global::System.Diagnostics.DebuggerNonUserCode]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
{"\t\t"}[Inject]
{"\t\t"}[EditorBrowsable(EditorBrowsableState.Never)]
{"\t\t"}private {@type} ____{AutoInjectHelper.FormatMemberName(name)} {{ get => {name}; set => {name} = value; }}";
{"\t\t"}private {typeDisplay} ____{AutoInjectHelper.FormatMemberName(name)} {{ get => {name}; set => {name} = value; }}";
}
Comment on lines +40 to 49
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for any AutoInject usage on fields (vs properties) to confirm the field path is exercised.
rg -nP '\[AutoInject\]\s*(?:private|protected|internal|public|readonly|static|\s)*\s+\w+[\w\.<>?,\s]*\s+_?\w+\s*;' --type=cs -g '!**/AutoInject/**'
echo '---'
# And confirm TypeDisplay format used in the generator.
rg -nP 'ToDisplayString\(' --type=cs -g 'src/**/AutoInject/AutoInjectSourceGenerator.cs'

Repository: bitfoundation/bitplatform

Length of output: 6227


🏁 Script executed:

#!/bin/bash
# 1. Check for tests covering AutoInject on fields
echo "=== Tests covering AutoInject on fields ==="
fd -e 'cs' -path '**/*Test*' -o -path '**/*test*' | xargs rg -l '\[AutoInject\]' 2>/dev/null | head -20

echo ""
echo "=== AutoInjectRazorComponentHandler property generation ==="
# Check how the property is generated and if it handles type qualification
rg -A5 'private static string GenerateProperty' 'src/BlazorUI/Bit.BlazorUI.SourceGenerators/AutoInject/AutoInjectRazorComponentHandler.cs'

echo ""
echo "=== Check how typeDisplay is used in the handler ==="
# See if typeDisplay is directly used in the template
rg -B5 -A10 'GenerateProperty' 'src/BlazorUI/Bit.BlazorUI.SourceGenerators/AutoInject/AutoInjectRazorComponentHandler.cs' | head -40

Repository: bitfoundation/bitplatform

Length of output: 3435


🏁 Script executed:

#!/bin/bash
# 2. Verify if default ToDisplayString() causes type qualification issues
echo "=== Check if TypeDisplay needs full qualification ==="
rg -B2 -A2 'AutoInjectMember.*TypeDisplay' 'src/BlazorUI/Bit.BlazorUI.SourceGenerators/AutoInject/'

echo ""
echo "=== Look for actual usage patterns in generated properties ==="
# Check if there are example generated outputs or documentation
fd -e 'md' -o -e 'txt' | xargs rg -l 'AutoInject' 2>/dev/null | head -5

Repository: bitfoundation/bitplatform

Length of output: 1112


🏁 Script executed:

#!/bin/bash
# Look for unit tests for AutoInject specifically
echo "=== Unit tests for AutoInject ==="
find . -path '**/*Test*' -o -path '**/*test*' | xargs grep -l 'AutoInject' 2>/dev/null | grep -i test

echo ""
echo "=== Check AutoInjectSourceGenerator for how it creates TypeDisplay ==="
# Show the exact lines where ToDisplayString is called for fields
sed -n '95,110p' 'src/BlazorUI/Bit.BlazorUI.SourceGenerators/AutoInject/AutoInjectSourceGenerator.cs'

echo ""
echo "=== Check if there's handling for types that need fully qualified names ==="
rg -i 'FullyQualified|MinimallyQualified|SymbolDisplayFormat' 'src/BlazorUI/Bit.BlazorUI.SourceGenerators/'

echo ""
echo "=== Check actual field usage with [AutoInject] ==="
# Find a real example and see what happens
rg -A3 '\[AutoInject\]\s*private\s+\w+' 'src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Controllers/FileUploadController.cs'

Repository: bitfoundation/bitplatform

Length of output: 1636


🏁 Script executed:

#!/bin/bash
# Check if there's any generated code that shows actual output
echo "=== Look for integration tests or sample outputs ==="
fd 'AutoInject' -e 'txt' -o -e 'g.cs' 2>/dev/null | head -10

echo ""
echo "=== Check handler files to see if they handle type qualification ==="
cat 'src/BlazorUI/Bit.BlazorUI.SourceGenerators/AutoInject/AutoInjectRazorComponentHandler.cs' | wc -l

echo ""
echo "=== Full AutoInjectRazorComponentHandler.cs to understand context ==="
cat 'src/BlazorUI/Bit.BlazorUI.SourceGenerators/AutoInject/AutoInjectRazorComponentHandler.cs'

Repository: bitfoundation/bitplatform

Length of output: 1826


🏁 Script executed:

#!/bin/bash
# Check if AppSettings is in scope for the FileUploadController
echo "=== Check AppSettings location and namespace ==="
rg -B5 'AppSettings' 'src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Controllers/FileUploadController.cs'

echo ""
echo "=== Check if there are actual compilation tests ==="
# Look for test files that verify generated code compiles
find . -path '*Test*' -name '*.cs' -type f | xargs grep -l 'AutoInject' 2>/dev/null | head -5

echo ""
echo "=== Check AutoInjectNormalClassHandler to see if it has different handling ==="
rg -A20 'private static string GenerateProperty' 'src/BlazorUI/Bit.BlazorUI.SourceGenerators/AutoInject/AutoInjectNormalClassHandler.cs' | head -25

Repository: bitfoundation/bitplatform

Length of output: 520


🏁 Script executed:

#!/bin/bash
# Check if there are any issues reported or TODOs in the AutoInject code
echo "=== Look for FIXMEs, TODOs, or issue comments ==="
rg -i 'todo|fixme|issue|bug|hack' 'src/BlazorUI/Bit.BlazorUI.SourceGenerators/AutoInject/'

echo ""
echo "=== Check if default ToDisplayString actually causes problems by looking at type names used ==="
# Check what types are typically used with [AutoInject]
rg '\[AutoInject\]\s*private\s+(\w+(?:<[^>]+>)?)\s+' -o --replace '$1' | sort | uniq | head -20

Repository: bitfoundation/bitplatform

Length of output: 2561


🏁 Script executed:

#!/bin/bash
# Check what namespaces are in the original files that use [AutoInject]
echo "=== Check using statements in FileUploadController ==="
head -20 'src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Controllers/FileUploadController.cs'

echo ""
echo "=== Check what namespace AppSettings is in ==="
rg -B5 'class AppSettings' 'src/BlazorUI/Demo/' | head -20

echo ""
echo "=== Verify the generated partial class structure ==="
# Check if there are actual generated files we can examine
find . -name '*.g.cs' -path '*AutoInject*' 2>/dev/null | head -5

echo ""
echo "=== Check if the generated files have all necessary usings ==="
# Look for how other generators handle this
rg -B3 -A10 'namespace.*{' 'src/BlazorUI/Bit.BlazorUI.SourceGenerators/AutoInject/AutoInjectNormalClassHandler.cs'

Repository: bitfoundation/bitplatform

Length of output: 1275


🏁 Script executed:

#!/bin/bash
# Check if the concern about missing usings in generated file is valid
echo "=== Look at a complete handler to see if usings are injected ==="
cat 'src/BlazorUI/Bit.BlazorUI.SourceGenerators/AutoInject/AutoInjectNormalClassHandler.cs'

Repository: bitfoundation/bitplatform

Length of output: 3403


🏁 Script executed:

#!/bin/bash
# Check if AutoInjectHelper.FormatMemberName properly handles leading underscores
echo "=== Check FormatMemberName implementation ==="
rg -A10 'FormatMemberName' 'src/BlazorUI/Bit.BlazorUI.SourceGenerators/AutoInject/AutoInjectHelper.cs'

echo ""
echo "=== Verify the generated property compiles for field with underscore ==="
# Look for any actual generated code output or documentation
find . -name '*g.cs' | xargs rg -l 'private.*___' 2>/dev/null | head -3

Repository: bitfoundation/bitplatform

Length of output: 593


🏁 Script executed:

#!/bin/bash
# Final check: does the generated RazorComponentHandler include needed usings?
echo "=== Check if generated code from RazorComponentHandler includes type usings ==="
# The template shows it adds System.ComponentModel and Microsoft.AspNetCore.Components
# but we need to check if it handles user-defined types

echo "Generated template from RazorComponentHandler:"
sed -n '11,28p' 'src/BlazorUI/Bit.BlazorUI.SourceGenerators/AutoInject/AutoInjectRazorComponentHandler.cs'

echo ""
echo "=== Check if similar types work without explicit using (same namespace) ==="
rg 'AppSettings' 'src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/' -B2 -A2 | head -15

Repository: bitfoundation/bitplatform

Length of output: 2052


Type qualification risk in generated properties — confirm cross-namespace types don't silently fail.

The generated property correctly handles field-to-property proxying (e.g., _settings____Settings proxies to _settings), and [AutoInject] on fields is actively used throughout the codebase. However, two substantive issues remain:

  1. Type qualification: AutoInjectMember.TypeDisplay uses Type.ToDisplayString() with no explicit format, which emits unqualified names (e.g., AppSettings instead of Bit.BlazorUI.Demo.Server.AppSettings). The generated partial class file adds only system usings (Microsoft.AspNetCore.Components, System.ComponentModel) but no dynamic usings for injected type namespaces. This works when types share the same namespace as the target class, but will cause compilation failures for cross-namespace types or complex generics. Consider using SymbolDisplayFormat.FullyQualifiedFormat when capturing TypeDisplay.

  2. Field path test coverage: While [AutoInject] on fields with leading underscores (e.g., _settings) exists in production code, the test suite coverage for this path is not evident beyond one reference. Verify that integration tests exercise field injection with various namespace and type scenarios.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/BlazorUI/Bit.BlazorUI.SourceGenerators/AutoInject/AutoInjectRazorComponentHandler.cs`
around lines 40 - 49, The generated property uses an unqualified type name from
AutoInjectMember.TypeDisplay which can break for cross-namespace or generic
types; update the code that computes TypeDisplay (where
AutoInjectMember.TypeDisplay is assigned) to call
symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) or otherwise
produce fully-qualified type names so GenerateProperty emits a fully qualified
type (refer to GenerateProperty and AutoInjectMember.TypeDisplay);
alternatively, emit appropriate using directives for the injected type namespace
when building the partial file. Also add/extend tests to exercise field-backed
injections (e.g., fields like _settings) with types defined in different
namespaces and with generics so the generator's output compiles in those
scenarios.

}

Loading
Loading