From 2f35007fc4c0ec62974303d22dfaca4917465f1e Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 8 May 2025 19:45:57 +0700 Subject: [PATCH 1/3] overloadedCase (simplest version) --- .../MacroExamples/MetaEnumMacro.swift | 23 +++++++++++++++---- .../MetaEnumMacroTests.swift | 20 ++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift b/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift index 22ac266..fe82a58 100644 --- a/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift @@ -29,6 +29,15 @@ public struct MetaEnumMacro { CaseMacroDiagnostic.notAnEnum(declaration).diagnose(at: Syntax(node)) ]) } + + let enumCaseDecls = enumDecl.memberBlock.members.compactMap { + $0.decl.as(EnumCaseDeclSyntax.self)?.elements.first + } + guard Set(enumCaseDecls.map(\.name.text)).count == enumCaseDecls.count else { + throw DiagnosticsError(diagnostics: [ + CaseMacroDiagnostic.overloadedCase.diagnose(at: Syntax(node)) + ]) + } parentTypeName = enumDecl.name.with(\.trailingTrivia, []) @@ -105,29 +114,33 @@ extension EnumDeclSyntax { } enum CaseMacroDiagnostic { + case overloadedCase case notAnEnum(DeclGroupSyntax) } extension CaseMacroDiagnostic: DiagnosticMessage { var message: String { switch self { + case .overloadedCase: + "'@MetaEnum' cannot be applied to enums with overloaded case names." case .notAnEnum(let decl): - return - "'@MetaEnum' can only be attached to an enum, not \(decl.descriptiveDeclKind(withArticle: true))" + "'@MetaEnum' can only be attached to an enum, not \(decl.descriptiveDeclKind(withArticle: true))" } } var diagnosticID: MessageID { switch self { + case .overloadedCase: + MessageID(domain: "MetaEnumDiagnostic", id: "overloadedCase") case .notAnEnum: - return MessageID(domain: "MetaEnumDiagnostic", id: "notAnEnum") + MessageID(domain: "MetaEnumDiagnostic", id: "notAnEnum") } } var severity: DiagnosticSeverity { switch self { - case .notAnEnum: - return .error + case .overloadedCase: .error + case .notAnEnum: .error } } diff --git a/Tests/MacroTestingTests/MetaEnumMacroTests.swift b/Tests/MacroTestingTests/MetaEnumMacroTests.swift index f12c0d4..71dd390 100644 --- a/Tests/MacroTestingTests/MetaEnumMacroTests.swift +++ b/Tests/MacroTestingTests/MetaEnumMacroTests.swift @@ -106,4 +106,24 @@ final class MetaEnumMacroTests: BaseTestCase { """ } } + + func testDuplicateCaseName() { + assertMacro { + """ + @MetaEnum enum Foo { + case bar(int: Int) + case bar(string: String) + } + """ + } diagnostics: { + """ + @MetaEnum enum Foo { + ┬──────── + ╰─ 🛑 '@MetaEnum' cannot be applied to enums with overloaded case names. + case bar(int: Int) + case bar(string: String) + } + """ + } + } } From 1dad99c2484b9afefb82b494ed69e3ac5d80b7db Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 8 May 2025 19:55:28 +0700 Subject: [PATCH 2/3] overloadedCase (advanced version) --- .../MacroExamples/MetaEnumMacro.swift | 17 +++++++----- .../MetaEnumMacroTests.swift | 26 ++++++++++++++++--- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift b/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift index fe82a58..4df18e5 100644 --- a/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift @@ -31,12 +31,17 @@ public struct MetaEnumMacro { } let enumCaseDecls = enumDecl.memberBlock.members.compactMap { - $0.decl.as(EnumCaseDeclSyntax.self)?.elements.first - } - guard Set(enumCaseDecls.map(\.name.text)).count == enumCaseDecls.count else { - throw DiagnosticsError(diagnostics: [ - CaseMacroDiagnostic.overloadedCase.diagnose(at: Syntax(node)) - ]) + $0.decl.as(EnumCaseDeclSyntax.self) + }.flatMap(\.elements) + + var seenCaseNames: Set = [] + for name in enumCaseDecls.map(\.name) { + defer { seenCaseNames.insert(name.text) } + guard !seenCaseNames.contains(name.text) else { + throw DiagnosticsError(diagnostics: [ + CaseMacroDiagnostic.overloadedCase.diagnose(at: Syntax(name)) + ]) + } } parentTypeName = enumDecl.name.with(\.trailingTrivia, []) diff --git a/Tests/MacroTestingTests/MetaEnumMacroTests.swift b/Tests/MacroTestingTests/MetaEnumMacroTests.swift index 71dd390..f419ee4 100644 --- a/Tests/MacroTestingTests/MetaEnumMacroTests.swift +++ b/Tests/MacroTestingTests/MetaEnumMacroTests.swift @@ -106,7 +106,7 @@ final class MetaEnumMacroTests: BaseTestCase { """ } } - + func testDuplicateCaseName() { assertMacro { """ @@ -118,12 +118,30 @@ final class MetaEnumMacroTests: BaseTestCase { } diagnostics: { """ @MetaEnum enum Foo { - ┬──────── - ╰─ 🛑 '@MetaEnum' cannot be applied to enums with overloaded case names. case bar(int: Int) case bar(string: String) + ┬── + ╰─ 🛑 '@MetaEnum' cannot be applied to enums with overloaded case names. } """ - } + } + } + + func testOverloadedCaseName_SingleLine() { + assertMacro { + """ + @MetaEnum enum Foo { + case bar(int: Int), bar(string: String) + } + """ + } diagnostics: { + """ + @MetaEnum enum Foo { + case bar(int: Int), bar(string: String) + ┬── + ╰─ 🛑 '@MetaEnum' cannot be applied to enums with overloaded case names. + } + """ + } } } From b4926905e9b39736978abcf82938cf50f6cba876 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 9 May 2025 08:55:45 +0700 Subject: [PATCH 3/3] fix notAnEnum error position --- .../MacroExamples/MetaEnumMacro.swift | 27 ++++--------------- .../MetaEnumMacroTests.swift | 4 +-- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift b/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift index 4df18e5..5688534 100644 --- a/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift +++ b/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift @@ -26,16 +26,12 @@ public struct MetaEnumMacro { ) throws { guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { throw DiagnosticsError(diagnostics: [ - CaseMacroDiagnostic.notAnEnum(declaration).diagnose(at: Syntax(node)) + CaseMacroDiagnostic.notAnEnum(declaration).diagnose(at: Syntax(declaration.introducer)) ]) } - let enumCaseDecls = enumDecl.memberBlock.members.compactMap { - $0.decl.as(EnumCaseDeclSyntax.self) - }.flatMap(\.elements) - var seenCaseNames: Set = [] - for name in enumCaseDecls.map(\.name) { + for name in enumDecl.caseElements.map(\.name) { defer { seenCaseNames.insert(name.text) } guard !seenCaseNames.contains(name.text) else { throw DiagnosticsError(diagnostics: [ @@ -156,21 +152,8 @@ extension CaseMacroDiagnostic: DiagnosticMessage { extension DeclGroupSyntax { func descriptiveDeclKind(withArticle article: Bool = false) -> String { - switch self { - case is ActorDeclSyntax: - return article ? "an actor" : "actor" - case is ClassDeclSyntax: - return article ? "a class" : "class" - case is ExtensionDeclSyntax: - return article ? "an extension" : "extension" - case is ProtocolDeclSyntax: - return article ? "a protocol" : "protocol" - case is StructDeclSyntax: - return article ? "a struct" : "struct" - case is EnumDeclSyntax: - return article ? "an enum" : "enum" - default: - fatalError("Unknown DeclGroupSyntax") - } + let introducerText = introducer.text + let prefix = article ? ["a", "e", "i", "o", "u"].contains(introducerText.first!) ? "an " : "a " : "" + return prefix + introducerText } } diff --git a/Tests/MacroTestingTests/MetaEnumMacroTests.swift b/Tests/MacroTestingTests/MetaEnumMacroTests.swift index f419ee4..c5c1443 100644 --- a/Tests/MacroTestingTests/MetaEnumMacroTests.swift +++ b/Tests/MacroTestingTests/MetaEnumMacroTests.swift @@ -97,8 +97,8 @@ final class MetaEnumMacroTests: BaseTestCase { } diagnostics: { """ @MetaEnum struct Cell { - ┬──────── - ╰─ 🛑 '@MetaEnum' can only be attached to an enum, not a struct + ┬───── + ╰─ 🛑 '@MetaEnum' can only be attached to an enum, not a struct let integer: Int let text: String let boolean: Bool