diff --git a/Directory.Build.props b/Directory.Build.props
index b3c6bb8..67b1383 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -6,4 +6,4 @@
AllEnabledByDefault
true
-
\ No newline at end of file
+
diff --git a/README.md b/README.md
index d97c056..4123138 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,7 @@ dotnet add package GodotSharpAnalyzers
- [x] **GD0001** (Memory): Signal connections should be disconnected to prevent memory leaks
- [x] **GD0001a** (Memory): Signal connected to lambda expression (informational)
+- [x] **GD0004** (TypeSafety): Classes using Godot source generators must be declared as partial
## Configuration
@@ -47,6 +48,7 @@ You can customize the analyzer behavior by setting MSBuild properties in your pr
true
true
+ true
```
diff --git a/docs/rules/GD0001.md b/docs/rules/GD0001.md
index fbdc2fd..07ecd48 100644
--- a/docs/rules/GD0001.md
+++ b/docs/rules/GD0001.md
@@ -134,4 +134,4 @@ You can configure this rule's behavior in your project file:
## References
- [Godot 4 C# Signal Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_signals.html)
-- [GitHub Issue #89116](https://github.com/godotengine/godot/issues/89116) - Signal memory leak discussion
\ No newline at end of file
+- [GitHub Issue #89116](https://github.com/godotengine/godot/issues/89116) - Signal memory leak discussion
diff --git a/docs/rules/GD0004.md b/docs/rules/GD0004.md
new file mode 100644
index 0000000..c949c22
--- /dev/null
+++ b/docs/rules/GD0004.md
@@ -0,0 +1,105 @@
+# GD0004: Missing partial modifier
+
+**Rule ID**: GD0004
+**Category**: TypeSafety
+**Severity**: Error
+
+## Cause
+
+A class that inherits from a Godot type and uses Godot source generators (signals, exports) is missing the `partial` modifier.
+
+## Rule Description
+
+Classes that use Godot's source generators must be declared as `partial` to allow the generated code to be merged with the class definition. This includes classes that:
+
+- Define signals using `[Signal]` attribute on delegates
+- Use `[Export]` attribute on properties or fields
+- Use other Godot source generation features
+
+## How to Fix Violations
+
+Add the `partial` modifier to the class declaration.
+
+## Examples
+
+### Violates GD0004
+
+```csharp
+using Godot;
+
+public class Player : Node // GD0004: Missing partial modifier
+{
+ [Signal]
+ public delegate void HealthChangedEventHandler(int newHealth);
+
+ [Export]
+ public int MaxHealth { get; set; } = 100;
+}
+```
+
+```csharp
+using Godot;
+
+public class Weapon : Node2D // GD0004: Missing partial modifier
+{
+ [Export]
+ public float damage = 25.0f;
+}
+```
+
+### Does Not Violate GD0004
+
+```csharp
+using Godot;
+
+public partial class Player : Node // Correct: partial modifier present
+{
+ [Signal]
+ public delegate void HealthChangedEventHandler(int newHealth);
+
+ [Export]
+ public int MaxHealth { get; set; } = 100;
+}
+```
+
+```csharp
+using Godot;
+
+public class SimpleNode : Node // Correct: no source generators used
+{
+ private int _health = 100;
+
+ public override void _Ready()
+ {
+ GD.Print("Ready!");
+ }
+}
+```
+
+```csharp
+public class RegularClass // Correct: doesn't inherit from Godot type
+{
+ public delegate void SomeEventHandler();
+ public int SomeProperty { get; set; }
+}
+```
+
+## When to Suppress Warnings
+
+This rule should generally not be suppressed, as missing the `partial` modifier will cause compilation errors when using Godot source generators.
+
+## Configuration
+
+This rule can be configured in your `.editorconfig` file:
+
+```ini
+# Disable GD0004 (not recommended)
+dotnet_diagnostic.GD0004.severity = none
+
+# Make GD0004 suggestions instead of errors (not recommended)
+dotnet_diagnostic.GD0004.severity = suggestion
+```
+
+## See Also
+
+- [Godot C# Source Generators Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_features.html#source-generators)
diff --git a/src/GodotSharpAnalyzers/AnalyzerReleases.Shipped.md b/src/GodotSharpAnalyzers/AnalyzerReleases.Shipped.md
index 339d370..f50bb1f 100644
--- a/src/GodotSharpAnalyzers/AnalyzerReleases.Shipped.md
+++ b/src/GodotSharpAnalyzers/AnalyzerReleases.Shipped.md
@@ -1,2 +1,2 @@
; Shipped analyzer releases
-; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
\ No newline at end of file
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
diff --git a/src/GodotSharpAnalyzers/AnalyzerReleases.Unshipped.md b/src/GodotSharpAnalyzers/AnalyzerReleases.Unshipped.md
index 8f735f8..a55e489 100644
--- a/src/GodotSharpAnalyzers/AnalyzerReleases.Unshipped.md
+++ b/src/GodotSharpAnalyzers/AnalyzerReleases.Unshipped.md
@@ -6,4 +6,5 @@
Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
GD0001 | Memory | Warning | SignalConnectionLeak, [SignalConnectionLeakAnalyzer](Analyzers/Memory/SignalConnectionLeakAnalyzer.cs)
-GD0001a | Memory | Info | SignalLambdaConnectionLeak, [SignalConnectionLeakAnalyzer](Analyzers/Memory/SignalConnectionLeakAnalyzer.cs)
\ No newline at end of file
+GD0001a | Memory | Info | SignalLambdaConnectionLeak, [SignalConnectionLeakAnalyzer](Analyzers/Memory/SignalConnectionLeakAnalyzer.cs)
+GD0004 | TypeSafety | Error | MissingPartialModifier, [PartialModifierAnalyzer](Analyzers/TypeSafety/PartialModifierAnalyzer.cs)
diff --git a/src/GodotSharpAnalyzers/Analyzers/Memory/SignalConnectionLeakAnalyzer.cs b/src/GodotSharpAnalyzers/Analyzers/Memory/SignalConnectionLeakAnalyzer.cs
index 54fdf11..a4c3b3c 100644
--- a/src/GodotSharpAnalyzers/Analyzers/Memory/SignalConnectionLeakAnalyzer.cs
+++ b/src/GodotSharpAnalyzers/Analyzers/Memory/SignalConnectionLeakAnalyzer.cs
@@ -234,4 +234,4 @@ private static bool IsSingletonClass(INamedTypeSymbol type)
return hasStaticInstance || hasAutoLoadAttribute;
}
-}
\ No newline at end of file
+}
diff --git a/src/GodotSharpAnalyzers/Analyzers/TypeSafety/PartialModifierAnalyzer.cs b/src/GodotSharpAnalyzers/Analyzers/TypeSafety/PartialModifierAnalyzer.cs
new file mode 100644
index 0000000..7248c2e
--- /dev/null
+++ b/src/GodotSharpAnalyzers/Analyzers/TypeSafety/PartialModifierAnalyzer.cs
@@ -0,0 +1,107 @@
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace GodotSharpAnalyzers.Analyzers.TypeSafety;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class PartialModifierAnalyzer : DiagnosticAnalyzer
+{
+ public override ImmutableArray SupportedDiagnostics
+ => ImmutableArray.Create(DiagnosticDescriptors.MissingPartialModifier);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeClass, SyntaxKind.ClassDeclaration);
+ }
+
+ private static void AnalyzeClass(SyntaxNodeAnalysisContext context)
+ {
+ var classDeclaration = (ClassDeclarationSyntax)context.Node;
+ var semanticModel = context.SemanticModel;
+
+ // Check if the class is already partial
+ if (classDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword))
+ return;
+
+ var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration);
+ if (classSymbol == null)
+ return;
+
+ // Check if it inherits from a Godot type
+ if (!InheritsFromGodotType(classSymbol))
+ return;
+
+ // Check if the class uses Godot source generators
+ if (UsesGodotSourceGenerators(classDeclaration, classSymbol))
+ {
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.MissingPartialModifier,
+ classDeclaration.Identifier.GetLocation(),
+ classSymbol.Name);
+
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ private static bool InheritsFromGodotType(INamedTypeSymbol type)
+ {
+ var current = type.BaseType;
+ while (current != null)
+ {
+ if (current.ContainingNamespace?.ToDisplayString() == "Godot")
+ return true;
+ current = current.BaseType;
+ }
+ return false;
+ }
+
+ private static bool UsesGodotSourceGenerators(ClassDeclarationSyntax classDeclaration, INamedTypeSymbol classSymbol)
+ {
+ // Check for [Signal] attribute on delegates
+ foreach (var member in classDeclaration.Members)
+ {
+ if (member is DelegateDeclarationSyntax delegateDecl)
+ {
+ if (HasSignalAttribute(delegateDecl))
+ return true;
+ }
+ }
+
+ // Check for [Export] attribute on properties/fields
+ var members = classSymbol.GetMembers();
+ foreach (var member in members)
+ {
+ if (member is IPropertySymbol || member is IFieldSymbol)
+ {
+ if (HasExportAttribute(member))
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool HasSignalAttribute(DelegateDeclarationSyntax delegateDeclaration)
+ {
+ return delegateDeclaration.AttributeLists
+ .SelectMany(list => list.Attributes)
+ .Any(attr =>
+ {
+ var name = attr.Name.ToString();
+ return name == "Signal" || name == "SignalAttribute";
+ });
+ }
+
+ private static bool HasExportAttribute(ISymbol member)
+ {
+ return member.GetAttributes()
+ .Any(attr => attr.AttributeClass?.Name == "ExportAttribute" &&
+ attr.AttributeClass.ContainingNamespace?.Name == "Godot");
+ }
+}
diff --git a/src/GodotSharpAnalyzers/CodeFixes/Memory/SignalConnectionLeakCodeFix.cs b/src/GodotSharpAnalyzers/CodeFixes/Memory/SignalConnectionLeakCodeFix.cs
index 86f1672..3c183bf 100644
--- a/src/GodotSharpAnalyzers/CodeFixes/Memory/SignalConnectionLeakCodeFix.cs
+++ b/src/GodotSharpAnalyzers/CodeFixes/Memory/SignalConnectionLeakCodeFix.cs
@@ -103,4 +103,4 @@ private async Task AddSignalDisconnectionAsync(
return editor.GetChangedDocument();
}
-}
\ No newline at end of file
+}
diff --git a/src/GodotSharpAnalyzers/CodeFixes/TypeSafety/PartialModifierCodeFix.cs b/src/GodotSharpAnalyzers/CodeFixes/TypeSafety/PartialModifierCodeFix.cs
new file mode 100644
index 0000000..97bebe6
--- /dev/null
+++ b/src/GodotSharpAnalyzers/CodeFixes/TypeSafety/PartialModifierCodeFix.cs
@@ -0,0 +1,67 @@
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace GodotSharpAnalyzers.CodeFixes.TypeSafety;
+
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(PartialModifierCodeFix)), Shared]
+public class PartialModifierCodeFix : CodeFixProvider
+{
+ public override ImmutableArray FixableDiagnosticIds
+ => ImmutableArray.Create(DiagnosticDescriptors.MissingPartialModifier.Id);
+
+ public override FixAllProvider GetFixAllProvider()
+ => WellKnownFixAllProviders.BatchFixer;
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ if (root == null)
+ return;
+
+ var diagnostic = context.Diagnostics.First();
+ var diagnosticSpan = diagnostic.Location.SourceSpan;
+
+ // Find the class declaration
+ var classDeclaration = root.FindToken(diagnosticSpan.Start)
+ .Parent?.AncestorsAndSelf()
+ .OfType()
+ .FirstOrDefault();
+
+ if (classDeclaration == null)
+ return;
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: "Add partial modifier",
+ createChangedDocument: c => AddPartialModifierAsync(context.Document, classDeclaration, c),
+ equivalenceKey: "AddPartialModifier"),
+ diagnostic);
+ }
+
+ private async Task AddPartialModifierAsync(
+ Document document,
+ ClassDeclarationSyntax classDeclaration,
+ CancellationToken cancellationToken)
+ {
+ var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+ if (root == null)
+ return document;
+
+ // Create new modifiers with partial
+ var newModifiers = classDeclaration.Modifiers.Add(SyntaxFactory.Token(SyntaxKind.PartialKeyword));
+
+ // Replace the class declaration with the new one
+ var newClassDeclaration = classDeclaration.WithModifiers(newModifiers);
+ var newRoot = root.ReplaceNode(classDeclaration, newClassDeclaration);
+
+ return document.WithSyntaxRoot(newRoot);
+ }
+}
diff --git a/src/GodotSharpAnalyzers/DiagnosticCategories.cs b/src/GodotSharpAnalyzers/DiagnosticCategories.cs
index cefbb30..e8f2a8d 100644
--- a/src/GodotSharpAnalyzers/DiagnosticCategories.cs
+++ b/src/GodotSharpAnalyzers/DiagnosticCategories.cs
@@ -7,4 +7,4 @@ public static class DiagnosticCategories
public const string Performance = "Performance";
public const string TypeSafety = "TypeSafety";
public const string BestPractices = "BestPractices";
-}
\ No newline at end of file
+}
diff --git a/src/GodotSharpAnalyzers/DiagnosticDescriptors.cs b/src/GodotSharpAnalyzers/DiagnosticDescriptors.cs
index 02fc969..4f10727 100644
--- a/src/GodotSharpAnalyzers/DiagnosticDescriptors.cs
+++ b/src/GodotSharpAnalyzers/DiagnosticDescriptors.cs
@@ -25,4 +25,14 @@ public static class DiagnosticDescriptors
isEnabledByDefault: true,
description: "Lambda expressions connected to signals cannot be disconnected easily. Consider using named methods for signals that need disconnection.",
helpLinkUri: string.Format(HelpLinkFormat, "GD0001"));
-}
\ No newline at end of file
+
+ public static readonly DiagnosticDescriptor MissingPartialModifier = new(
+ id: "GD0004",
+ title: "Missing partial modifier",
+ messageFormat: "Class '{0}' uses Godot source generators and must be declared as partial",
+ category: DiagnosticCategories.TypeSafety,
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "Classes using Godot source generators require the partial modifier.",
+ helpLinkUri: string.Format(HelpLinkFormat, "GD0004"));
+}
diff --git a/src/GodotSharpAnalyzers/build/GodotSharpAnalyzers.props b/src/GodotSharpAnalyzers/build/GodotSharpAnalyzers.props
index 6227eac..fc37d72 100644
--- a/src/GodotSharpAnalyzers/build/GodotSharpAnalyzers.props
+++ b/src/GodotSharpAnalyzers/build/GodotSharpAnalyzers.props
@@ -15,6 +15,7 @@
$(NoWarn);GD0001
$(NoWarn);GD0001a
+ $(NoWarn);GD0004
@@ -31,4 +32,4 @@
-
\ No newline at end of file
+
diff --git a/tests/GodotSharpAnalyzers.Tests/Memory/SignalConnectionLeakAnalyzerTests.cs b/tests/GodotSharpAnalyzers.Tests/Memory/SignalConnectionLeakAnalyzerTests.cs
index 188c2f3..666b00a 100644
--- a/tests/GodotSharpAnalyzers.Tests/Memory/SignalConnectionLeakAnalyzerTests.cs
+++ b/tests/GodotSharpAnalyzers.Tests/Memory/SignalConnectionLeakAnalyzerTests.cs
@@ -194,4 +194,4 @@ public override void _Ready()
// Should not trigger for static method connections
await VerifyAnalyzer.VerifyAnalyzerAsync(test);
}
-}
\ No newline at end of file
+}
diff --git a/tests/GodotSharpAnalyzers.Tests/Memory/SignalConnectionLeakConfigurationTests.cs b/tests/GodotSharpAnalyzers.Tests/Memory/SignalConnectionLeakConfigurationTests.cs
index c92453f..d62ccef 100644
--- a/tests/GodotSharpAnalyzers.Tests/Memory/SignalConnectionLeakConfigurationTests.cs
+++ b/tests/GodotSharpAnalyzers.Tests/Memory/SignalConnectionLeakConfigurationTests.cs
@@ -328,4 +328,4 @@ private void OnHealthChanged(int newHealth)
}
}.RunAsync();
}
-}
\ No newline at end of file
+}
diff --git a/tests/GodotSharpAnalyzers.Tests/Memory/SignalConnectionLeakEdgeCaseTests.cs b/tests/GodotSharpAnalyzers.Tests/Memory/SignalConnectionLeakEdgeCaseTests.cs
index fd803cc..7edf62c 100644
--- a/tests/GodotSharpAnalyzers.Tests/Memory/SignalConnectionLeakEdgeCaseTests.cs
+++ b/tests/GodotSharpAnalyzers.Tests/Memory/SignalConnectionLeakEdgeCaseTests.cs
@@ -299,4 +299,4 @@ private void OnHealthChanged(int newHealth) { }
await VerifyAnalyzer.VerifyAnalyzerAsync(test, expected);
}
-}
\ No newline at end of file
+}
diff --git a/tests/GodotSharpAnalyzers.Tests/TypeSafety/PartialModifierAnalyzerTests.cs b/tests/GodotSharpAnalyzers.Tests/TypeSafety/PartialModifierAnalyzerTests.cs
new file mode 100644
index 0000000..90e5969
--- /dev/null
+++ b/tests/GodotSharpAnalyzers.Tests/TypeSafety/PartialModifierAnalyzerTests.cs
@@ -0,0 +1,432 @@
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Testing;
+using Xunit;
+using VerifyCS = GodotSharpAnalyzers.Tests.Verifiers.CSharpCodeFixVerifier<
+ GodotSharpAnalyzers.Analyzers.TypeSafety.PartialModifierAnalyzer,
+ GodotSharpAnalyzers.CodeFixes.TypeSafety.PartialModifierCodeFix>;
+using VerifyAnalyzer = GodotSharpAnalyzers.Tests.Verifiers.CSharpAnalyzerVerifier<
+ GodotSharpAnalyzers.Analyzers.TypeSafety.PartialModifierAnalyzer>;
+
+namespace GodotSharpAnalyzers.Tests.TypeSafety;
+
+public class PartialModifierAnalyzerTests
+{
+ [Fact]
+ public async Task TestClassWithSignalMissingPartial()
+ {
+ var test = @"
+using Godot;
+
+public class {|#0:Player|} : Node
+{
+ [Signal]
+ public delegate void HealthChangedEventHandler(int newHealth);
+}";
+
+ var fixedTest = @"
+using Godot;
+
+public partial class Player : Node
+{
+ [Signal]
+ public delegate void HealthChangedEventHandler(int newHealth);
+}";
+
+ var expected = VerifyCS.Diagnostic(DiagnosticDescriptors.MissingPartialModifier)
+ .WithLocation(0)
+ .WithArguments("Player");
+
+ await VerifyCS.VerifyCodeFixAsync(test, expected, fixedTest);
+ }
+
+ [Fact]
+ public async Task TestClassWithExportMissingPartial()
+ {
+ var test = @"
+using Godot;
+
+public class {|#0:Enemy|} : Node
+{
+ [Export]
+ public int Health { get; set; } = 100;
+}";
+
+ var fixedTest = @"
+using Godot;
+
+public partial class Enemy : Node
+{
+ [Export]
+ public int Health { get; set; } = 100;
+}";
+
+ var expected = VerifyCS.Diagnostic(DiagnosticDescriptors.MissingPartialModifier)
+ .WithLocation(0)
+ .WithArguments("Enemy");
+
+ await VerifyCS.VerifyCodeFixAsync(test, expected, fixedTest);
+ }
+
+ [Fact]
+ public async Task TestPartialClassNoWarning()
+ {
+ var test = @"
+using Godot;
+
+public partial class Player : Node
+{
+ [Signal]
+ public delegate void HealthChangedEventHandler(int newHealth);
+
+ [Export]
+ public int MaxHealth { get; set; } = 100;
+}";
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(test);
+ }
+
+ [Fact]
+ public async Task TestNonGodotClassNoWarning()
+ {
+ var test = @"
+public class RegularClass
+{
+ public delegate void SomeEventHandler();
+
+ public int SomeProperty { get; set; }
+}";
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(test);
+ }
+
+ [Fact]
+ public async Task TestGodotClassWithoutSourceGeneratorsNoWarning()
+ {
+ var test = @"
+using Godot;
+
+public class Player : Node
+{
+ // No signals or exports
+ private int _health = 100;
+
+ public override void _Ready()
+ {
+ GD.Print(""Player ready"");
+ }
+}";
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(test);
+ }
+
+ [Fact]
+ public async Task TestClassWithExportFieldMissingPartial()
+ {
+ var test = @"
+using Godot;
+
+public class {|#0:Weapon|} : Node
+{
+ [Export]
+ public float damage = 50.0f;
+}";
+
+ var fixedTest = @"
+using Godot;
+
+public partial class Weapon : Node
+{
+ [Export]
+ public float damage = 50.0f;
+}";
+
+ var expected = VerifyCS.Diagnostic(DiagnosticDescriptors.MissingPartialModifier)
+ .WithLocation(0)
+ .WithArguments("Weapon");
+
+ await VerifyCS.VerifyCodeFixAsync(test, expected, fixedTest);
+ }
+
+ [Fact]
+ public async Task TestClassWithMultipleModifiersPartialAdded()
+ {
+ var test = @"
+using Godot;
+
+public abstract class {|#0:BasePlayer|} : Node
+{
+ [Signal]
+ public delegate void PlayerDiedEventHandler();
+}";
+
+ var fixedTest = @"
+using Godot;
+
+public abstract partial class BasePlayer : Node
+{
+ [Signal]
+ public delegate void PlayerDiedEventHandler();
+}";
+
+ var expected = VerifyCS.Diagnostic(DiagnosticDescriptors.MissingPartialModifier)
+ .WithLocation(0)
+ .WithArguments("BasePlayer");
+
+ await VerifyCS.VerifyCodeFixAsync(test, expected, fixedTest);
+ }
+
+ [Fact]
+ public async Task TestInheritanceFromGodotResource()
+ {
+ var test = @"
+using Godot;
+
+public class {|#0:CustomResource|} : Resource
+{
+ [Export]
+ public string ResourceName { get; set; } = """";
+}";
+
+ var fixedTest = @"
+using Godot;
+
+public partial class CustomResource : Resource
+{
+ [Export]
+ public string ResourceName { get; set; } = """";
+}";
+
+ var expected = VerifyCS.Diagnostic(DiagnosticDescriptors.MissingPartialModifier)
+ .WithLocation(0)
+ .WithArguments("CustomResource");
+
+ await VerifyCS.VerifyCodeFixAsync(test, expected, fixedTest);
+ }
+
+ [Fact]
+ public async Task TestNestedClassWithGodotInheritance()
+ {
+ var test = @"
+using Godot;
+
+public class OuterClass
+{
+ public class {|#0:NestedPlayer|} : Node
+ {
+ [Signal]
+ public delegate void NestedSignalEventHandler();
+ }
+}";
+
+ var fixedTest = @"
+using Godot;
+
+public class OuterClass
+{
+ public partial class NestedPlayer : Node
+ {
+ [Signal]
+ public delegate void NestedSignalEventHandler();
+ }
+}";
+
+ var expected = VerifyCS.Diagnostic(DiagnosticDescriptors.MissingPartialModifier)
+ .WithLocation(0)
+ .WithArguments("NestedPlayer");
+
+ await VerifyCS.VerifyCodeFixAsync(test, expected, fixedTest);
+ }
+
+ [Fact]
+ public async Task TestFalsePositiveWithCustomGodotNamespace()
+ {
+ var test = @"
+namespace MyProject.Godot
+{
+ public class Node
+ {
+ // Custom Node class in custom Godot namespace
+ }
+}
+
+namespace MyProject
+{
+ using MyProject.Godot;
+
+ public class Player : Node // Should not trigger - not the real Godot.Node
+ {
+ public delegate void SomeEventHandler();
+
+ public int SomeProperty { get; set; }
+ }
+}";
+
+ // Should not produce any diagnostics
+ await VerifyAnalyzer.VerifyAnalyzerAsync(test);
+ }
+
+ [Fact]
+ public async Task TestDeepInheritanceFromGodotType()
+ {
+ var test = @"
+using Godot;
+
+public class BaseCharacter : Node
+{
+ protected int _health;
+}
+
+public class {|#0:Player|} : BaseCharacter
+{
+ [Export]
+ public int MaxHealth { get; set; } = 100;
+}";
+
+ var fixedTest = @"
+using Godot;
+
+public class BaseCharacter : Node
+{
+ protected int _health;
+}
+
+public partial class Player : BaseCharacter
+{
+ [Export]
+ public int MaxHealth { get; set; } = 100;
+}";
+
+ var expected = VerifyCS.Diagnostic(DiagnosticDescriptors.MissingPartialModifier)
+ .WithLocation(0)
+ .WithArguments("Player");
+
+ await VerifyCS.VerifyCodeFixAsync(test, expected, fixedTest);
+ }
+
+ [Fact]
+ public async Task TestClassWithBothSignalAndExport()
+ {
+ var test = @"
+using Godot;
+
+public class {|#0:ComplexNode|} : Node
+{
+ [Signal]
+ public delegate void StateChangedEventHandler(string newState);
+
+ [Export]
+ public float Speed { get; set; } = 100.0f;
+
+ [Export]
+ private int _maxItems = 10;
+}";
+
+ var fixedTest = @"
+using Godot;
+
+public partial class ComplexNode : Node
+{
+ [Signal]
+ public delegate void StateChangedEventHandler(string newState);
+
+ [Export]
+ public float Speed { get; set; } = 100.0f;
+
+ [Export]
+ private int _maxItems = 10;
+}";
+
+ var expected = VerifyCS.Diagnostic(DiagnosticDescriptors.MissingPartialModifier)
+ .WithLocation(0)
+ .WithArguments("ComplexNode");
+
+ await VerifyCS.VerifyCodeFixAsync(test, expected, fixedTest);
+ }
+
+
+ [Fact]
+ public async Task TestSimpleNodeInheritanceNoWarning()
+ {
+ var test = @"
+using Godot;
+
+public class SimplePlayer : Node // Should this trigger? Let's test!
+{
+ private int _health = 100;
+
+ public override void _Ready()
+ {
+ GD.Print(""Player ready with health: "" + _health);
+ }
+
+ public override void _Process(double delta)
+ {
+ // Some game logic
+ }
+}";
+
+ // If this passes, then simple inheritance doesn't require partial
+ await VerifyAnalyzer.VerifyAnalyzerAsync(test);
+ }
+
+ [Fact]
+ public async Task TestVirtualMethodOverridesOnlyNoWarning()
+ {
+ var test = @"
+using Godot;
+
+public class GameManager : Node // Should this trigger? Let's test!
+{
+ public override void _Ready()
+ {
+ GD.Print(""Game Manager Ready"");
+ }
+
+ public override void _Process(double delta)
+ {
+ // Some simple game logic
+ GD.Print(""Processing: "" + delta);
+ }
+}";
+
+ // If this passes, then virtual method overrides don't require partial
+ await VerifyAnalyzer.VerifyAnalyzerAsync(test);
+ }
+
+ [Fact]
+ public async Task TestRegularCSharpClassWithSignalAttributeNoWarning()
+ {
+ var test = @"
+using Godot;
+
+public class RegularClass // NOT inheriting from Godot - should NOT trigger!
+{
+ [Signal]
+ public delegate void SomeEventEventHandler();
+
+ public int SomeProperty { get; set; }
+}";
+
+ // Should not produce any diagnostics - regular classes don't need partial
+ await VerifyAnalyzer.VerifyAnalyzerAsync(test);
+ }
+
+ [Fact]
+ public async Task TestRegularCSharpClassWithExportAttributeNoWarning()
+ {
+ var test = @"
+using Godot;
+
+public class DataClass // NOT inheriting from Godot - should NOT trigger!
+{
+ [Export]
+ public string Name { get; set; } = """";
+
+ [Export]
+ public int Value = 42;
+}";
+
+ // Should not produce any diagnostics - regular classes don't need partial
+ await VerifyAnalyzer.VerifyAnalyzerAsync(test);
+ }
+}
diff --git a/tests/GodotSharpAnalyzers.Tests/Verifiers/CSharpAnalyzerVerifier.cs b/tests/GodotSharpAnalyzers.Tests/Verifiers/CSharpAnalyzerVerifier.cs
index f33d2a1..0b75e80 100644
--- a/tests/GodotSharpAnalyzers.Tests/Verifiers/CSharpAnalyzerVerifier.cs
+++ b/tests/GodotSharpAnalyzers.Tests/Verifiers/CSharpAnalyzerVerifier.cs
@@ -396,4 +396,4 @@ private static ImmutableDictionary GetNullableWarnings
builder.Add("CS8714", ReportDiagnostic.Error);
return builder.ToImmutable();
}
-}
\ No newline at end of file
+}
diff --git a/tests/GodotSharpAnalyzers.Tests/Verifiers/CSharpCodeFixVerifier.cs b/tests/GodotSharpAnalyzers.Tests/Verifiers/CSharpCodeFixVerifier.cs
index 5789be0..e064631 100644
--- a/tests/GodotSharpAnalyzers.Tests/Verifiers/CSharpCodeFixVerifier.cs
+++ b/tests/GodotSharpAnalyzers.Tests/Verifiers/CSharpCodeFixVerifier.cs
@@ -323,4 +323,4 @@ private static string GetTestNodeWithSignal()
{
return Test.GetTestNodeWithSignal();
}
-}
\ No newline at end of file
+}