From 566b727c65a883a0347b27ca72ec202f5a6ff108 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 20:30:45 +0000 Subject: [PATCH] Write a source file's global attributes after its usings [patch] AddSourceFile wrote the template's attributes first, so a file with both attributes and usings put [assembly: X] above the usings (CS1529) or outside the file-scoped namespace the usings were in (CS0246). When the file has attributes it now writes the usings, then the attributes, then the namespace. A file without attributes renders as before. Fixes #143 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW3gZxmT7yZRzkmnHAX --- CodeBlocker.Test/TemplateTests.cs | 31 +++++++++++++++++++++ CodeBlocker/Templates/SourceFileTemplate.cs | 27 ++++++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/CodeBlocker.Test/TemplateTests.cs b/CodeBlocker.Test/TemplateTests.cs index 999de40..dae094a 100644 --- a/CodeBlocker.Test/TemplateTests.cs +++ b/CodeBlocker.Test/TemplateTests.cs @@ -1094,6 +1094,37 @@ public void ASourceFileWritesItsPreambleThenItsTypes() Render(codeBlocker => codeBlocker.AddSourceFile(file))); } + [TestMethod] + public void ASourceFileWritesItsGlobalAttributesAfterItsUsingsAndBeforeItsNamespace() + { + SourceFileTemplate file = new() + { + Namespace = "Contoso", + Usings = { "System" }, + Attributes = { "assembly: CLSCompliant(true)" }, + Classes = { new ClassTemplate { Name = "Widget", Keywords = { "public" } } }, + }; + + Assert.AreEqual( + "using System;\n\n[assembly: CLSCompliant(true)]\n\nnamespace Contoso;\n\npublic class Widget\n{\n}\n\n", + Render(codeBlocker => codeBlocker.AddSourceFile(file))); + } + + [TestMethod] + public void ASourceFileWithNoNamespaceWritesItsGlobalAttributesAfterItsUsings() + { + SourceFileTemplate file = new() + { + Usings = { "System" }, + Attributes = { "assembly: CLSCompliant(true)" }, + Classes = { new ClassTemplate { Name = "Widget", Keywords = { "public" } } }, + }; + + Assert.AreEqual( + "using System;\n\n[assembly: CLSCompliant(true)]\n\npublic class Widget\n{\n}\n\n", + Render(codeBlocker => codeBlocker.AddSourceFile(file))); + } + #endregion #region Null handling diff --git a/CodeBlocker/Templates/SourceFileTemplate.cs b/CodeBlocker/Templates/SourceFileTemplate.cs index cb15d10..9b644f9 100644 --- a/CodeBlocker/Templates/SourceFileTemplate.cs +++ b/CodeBlocker/Templates/SourceFileTemplate.cs @@ -39,6 +39,13 @@ public static class SourceFileTemplateExtensions /// The to write to. /// The file to write. /// The same , for chaining. + /// + /// The only attributes a file can carry are global ones (assembly:, module:), and + /// C# only accepts them after every using directive and before the namespace. So when the file + /// has attributes, its usings come first, then the attributes, then the namespace; that is the one + /// order in which an attribute can be named through the file's own usings. A file with no + /// attributes keeps the usings inside its file-scoped namespace. + /// /// /// or is . /// @@ -47,9 +54,23 @@ public static CodeBlocker AddSourceFile(this CodeBlocker codeBlocker, SourceFile Ensure.NotNull(codeBlocker); Ensure.NotNull(template); - codeBlocker.AddTemplate(template); - codeBlocker.WriteFileScopedNamespace(template.Namespace); - codeBlocker.WriteUsings(template.Usings); + codeBlocker.AddDocumentation(template.Documentation); + codeBlocker.AddComments(template.Comments); + if (template.Attributes.Count > 0) + { + codeBlocker.WriteUsings(template.Usings); + codeBlocker.AddAttributes(template.Attributes); + codeBlocker.NewLine(); + codeBlocker.AddKeywords(template.Keywords); + codeBlocker.WriteFileScopedNamespace(template.Namespace); + } + else + { + codeBlocker.AddKeywords(template.Keywords); + codeBlocker.WriteFileScopedNamespace(template.Namespace); + codeBlocker.WriteUsings(template.Usings); + } + codeBlocker.AddClasses(template.Classes); return codeBlocker; }