diff --git a/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift b/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift index e7cf541..62f4ae3 100644 --- a/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift +++ b/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift @@ -23,7 +23,12 @@ public struct MemberwiseInitMacro: MemberMacro { ) throws -> [SwiftSyntax.DeclSyntax] where D: DeclGroupSyntax, C: MacroExpansionContext { guard [SwiftSyntax.SyntaxKind.classDecl, .structDecl, .actorDecl].contains(decl.kind) else { - throw MemberwiseInitMacroDiagnostic.invalidDeclarationKind(decl) + throw MacroExpansionErrorMessage( + """ + @MemberwiseInit can only be attached to a struct, class, or actor; \ + not to \(decl.descriptiveDeclKind(withArticle: true)). + """ + ) } deprecationDiagnostics(node: node, declaration: decl) @@ -153,17 +158,8 @@ public struct MemberwiseInitMacro: MemberMacro { !variable.isComputedProperty else { return } - if variable.customConfigurationAttributes.count > 1 { - acc.diagnostics += variable.customConfigurationAttributes.dropFirst().map { attribute in - Diagnostic( - node: attribute, - message: MacroExpansionErrorMessage( - """ - Multiple @Init configurations are not supported by @MemberwiseInit - """ - ) - ) - } + if let diagnostics = diagnoseMultipleConfigurations(variable: variable) { + acc.diagnostics += diagnostics return } @@ -172,52 +168,11 @@ public struct MemberwiseInitMacro: MemberMacro { return } - var diagnostics = [Diagnostic]() - - if let customSettings = customSettings { - if customSettings.label?.isInvalidSwiftLabel ?? false { - diagnostics.append(customSettings.diagnosticOnLabelValue(message: .invalidSwiftLabel)) - } else if let label = customSettings.label, - label != "_", - variable.bindings.count > 1 - { - diagnostics.append( - customSettings.diagnosticOnLabel(message: .labelAppliedToMultipleBindings)) - } - - if customSettings.defaultValue != nil, variable.bindings.count > 1 { - diagnostics.append( - customSettings.diagnosticOnDefault(message: .defaultAppliedToMultipleBindings) - ) - } - } - - // TODO: repetition of logic for custom configuration logic - let effectiveAccessLevel = customSettings?.accessLevel ?? variable.accessLevel - if targetAccessLevel > effectiveAccessLevel, - !variable.isFullyInitializedLet - { - let customAccess = variable.customConfigurationArguments? - .first? - .expression - .as(MemberAccessExprSyntax.self) - - let targetNode = - customAccess?._syntaxNode - ?? (variable.modifiers.isEmpty ? variable._syntaxNode : variable.modifiers._syntaxNode) - - diagnostics += [ - Diagnostic( - node: targetNode, - message: MacroExpansionErrorMessage( - """ - @MemberwiseInit(.\(targetAccessLevel)) would leak access to '\(effectiveAccessLevel)' property - """ - ) - ) - ] - } - + let diagnostics = diagnoseVariableDecl( + customSettings: customSettings, + variable: variable, + targetAccessLevel: targetAccessLevel + ) guard diagnostics.isEmpty else { acc.diagnostics += diagnostics return @@ -232,54 +187,6 @@ public struct MemberwiseInitMacro: MemberMacro { } } - private static func customInitLabelDiagnosticsFor(bindings: [PropertyBinding]) -> [Diagnostic] { - var diagnostics: [Diagnostic] = [] - - let customLabeledBindings = bindings.filter { - $0.variable.customSettings?.label != nil - } - - // Diagnose custom label conflicts with another custom label - var seenCustomLabels: Set = [] - for binding in customLabeledBindings { - guard - let customSettings = binding.variable.customSettings, - let label = customSettings.label, - label != "_" - else { continue } - defer { seenCustomLabels.insert(label) } - if seenCustomLabels.contains(label) { - diagnostics.append( - customSettings.diagnosticOnLabelValue(message: .labelConflictsWithAnotherLabel(label)) - ) - } - } - - return diagnostics - } - - private static func customInitLabelDiagnosticsFor(properties: [MemberProperty]) -> [Diagnostic] { - var diagnostics: [Diagnostic] = [] - - let propertiesByName = Dictionary(uniqueKeysWithValues: properties.map { ($0.name, $0) }) - - // Diagnose custom label conflicts with a property - for property in properties { - guard - let propertyCustomSettings = property.customSettings, - let label = propertyCustomSettings.label, - let duplicated = propertiesByName[label], - duplicated != property - else { continue } - - diagnostics.append( - propertyCustomSettings.diagnosticOnLabelValue(message: .labelConflictsWithProperty(label)) - ) - } - - return diagnostics - } - private static func collectPropertyBindings(variables: [MemberVariable]) -> [PropertyBinding] { variables.flatMap { variable -> [PropertyBinding] in variable.bindings @@ -322,11 +229,25 @@ public struct MemberwiseInitMacro: MemberMacro { } if propertyBinding.isInitializedVarWithoutType { - acc.diagnostics.append(propertyBinding.diagnostic(message: .missingTypeForVarProperty)) + acc.diagnostics.append( + propertyBinding.diagnostic( + MacroExpansionErrorMessage("@MemberwiseInit requires a type annotation.") + ) + ) return } if propertyBinding.isTuplePattern { - acc.diagnostics.append(propertyBinding.diagnostic(message: .tupleDestructuringInProperty)) + acc.diagnostics.append( + propertyBinding.diagnostic( + MacroExpansionErrorMessage( + """ + @MemberwiseInit does not support tuple destructuring for property declarations. \ + Use multiple declarations instead. + """ + ) + ) + ) + return } @@ -479,158 +400,3 @@ public struct MemberwiseInitMacro: MemberMacro { return "\(assignee) = \(parameterName)" } } - -private struct VariableCustomSettings: Equatable { - enum Assignee: Equatable { - case wrapper - case raw(String) - } - - let accessLevel: AccessLevelModifier? - let assignee: Assignee? - let defaultValue: String? - let forceEscaping: Bool - let ignore: Bool - let label: String? - let type: TypeSyntax? - let _syntaxNode: AttributeSyntax - - func diagnosticOnDefault(message: MemberwiseInitMacroDiagnostic) -> Diagnostic { - let labelNode = self._syntaxNode - .arguments? - .as(LabeledExprListSyntax.self)? - .firstWhereLabel("default") - - return diagnostic(node: labelNode ?? self._syntaxNode, message: message) - } - - func diagnosticOnLabel(message: MemberwiseInitMacroDiagnostic) -> Diagnostic { - let labelNode = self._syntaxNode - .arguments? - .as(LabeledExprListSyntax.self)? - .firstWhereLabel("label") - - return diagnostic(node: labelNode ?? self._syntaxNode, message: message) - } - - func diagnosticOnLabelValue(message: MemberwiseInitMacroDiagnostic) -> Diagnostic { - let labelValueNode = self._syntaxNode - .arguments? - .as(LabeledExprListSyntax.self)? - .firstWhereLabel("label")? - .expression - - return diagnostic(node: labelValueNode ?? self._syntaxNode, message: message) - } - - private func diagnostic( - node: any SyntaxProtocol, - message: MemberwiseInitMacroDiagnostic - ) -> Diagnostic { - Diagnostic(node: node, message: message) - } -} - -private struct PropertyBinding { - let typeFromTrailingBinding: TypeSyntax? - let syntax: PatternBindingSyntax - let variable: MemberVariable - - var effectiveType: TypeSyntax? { - variable.customSettings?.type - ?? self.syntax.typeAnnotation?.type - ?? self.syntax.initializer?.value.inferredTypeSyntax - ?? self.typeFromTrailingBinding - } - - var initializerValue: ExprSyntax? { - self.syntax.initializer?.trimmed.value - } - - var isComputedProperty: Bool { - self.syntax.isComputedProperty - } - - var isTuplePattern: Bool { - self.syntax.pattern.isTuplePattern - } - - // TODO: think carefully about how to improve this situation - var name: String? { - self.syntax.pattern.as(IdentifierPatternSyntax.self)?.identifier.text - } - - var isInitializedVarWithoutType: Bool { - self.initializerValue != nil - && self.variable.keywordToken == .keyword(.var) - && self.effectiveType == nil - && self.initializerValue?.inferredTypeSyntax == nil - } - - var isInitializedLet: Bool { - self.initializerValue != nil && self.variable.keywordToken == .keyword(.let) - } - - func diagnostic(message: MemberwiseInitMacroDiagnostic) -> Diagnostic { - Diagnostic(node: self.syntax._syntaxNode, message: message) - } -} - -private struct MemberVariable { - let customSettings: VariableCustomSettings? - let syntax: VariableDeclSyntax - - var accessLevel: AccessLevelModifier { - self.syntax.accessLevel - } - - var bindings: PatternBindingListSyntax { - self.syntax.bindings - } - - var keywordToken: TokenKind { - self.syntax.bindingSpecifier.tokenKind - } - - var _syntaxNode: Syntax { - self.syntax._syntaxNode - } -} - -private struct MemberProperty: Equatable { - let accessLevel: AccessLevelModifier - let customSettings: VariableCustomSettings? - let initializerValue: ExprSyntax? - let keywordToken: TokenKind - let name: String - let type: TypeSyntax - - func initParameterLabel( - considering allProperties: [MemberProperty], - deunderscoreParameters: Bool - ) -> String { - guard - let customSettings = self.customSettings, - customSettings.label - != self.initParameterName( - considering: allProperties, - deunderscoreParameters: deunderscoreParameters - ) - else { return "" } - - return customSettings.label.map { "\($0) " } ?? "" - } - - func initParameterName( - considering allProperties: [MemberProperty], - deunderscoreParameters: Bool - ) -> String { - guard - self.customSettings?.label == nil, - deunderscoreParameters - else { return self.name } - - let potentialName = self.name.hasPrefix("_") ? String(name.dropFirst()) : self.name - return allProperties.contains(where: { $0.name == potentialName }) ? self.name : potentialName - } -} diff --git a/Sources/MemberwiseInitMacros/Macros/Support/Diagnostics.swift b/Sources/MemberwiseInitMacros/Macros/Support/Diagnostics.swift index 40d75f8..029284d 100644 --- a/Sources/MemberwiseInitMacros/Macros/Support/Diagnostics.swift +++ b/Sources/MemberwiseInitMacros/Macros/Support/Diagnostics.swift @@ -1,105 +1,164 @@ import SwiftDiagnostics import SwiftSyntax +import SwiftSyntaxMacroExpansion -enum MemberwiseInitMacroDiagnostic: Error, DiagnosticMessage { - case defaultAppliedToMultipleBindings - case labelAppliedToMultipleBindings - case labelConflictsWithProperty(String) - case labelConflictsWithAnotherLabel(String) - case invalidDeclarationKind(DeclGroupSyntax) - case invalidSwiftLabel - case missingTypeForVarProperty - case tupleDestructuringInProperty +// MARK: - Diagnose VariableDeclSyntax - private var rawValue: String { - switch self { - case .defaultAppliedToMultipleBindings: - ".defaultAppliedToMultipleBindings" +func diagnoseMultipleConfigurations(variable: VariableDeclSyntax) -> [Diagnostic]? { + guard variable.customConfigurationAttributes.count > 1 else { return nil } - case .labelAppliedToMultipleBindings: - ".labelAppliedToMultipleBindings" - - case .invalidDeclarationKind(let declGroup): - ".invalidDeclarationKind(\(declGroup.kind))" - - case .invalidSwiftLabel: - ".invalidLabel" - - case .labelConflictsWithProperty(let label): - ".labelCollidesWithProperty(\(label))" - - case .labelConflictsWithAnotherLabel(let label): - ".labelCollidesWithAnotherLabel(\(label))" + return variable.customConfigurationAttributes.dropFirst().map { attribute in + Diagnostic( + node: attribute, + message: MacroExpansionErrorMessage( + "Multiple @Init configurations are not supported by @MemberwiseInit" + ) + ) + } +} - case .missingTypeForVarProperty: - ".missingTypeForVarProperty" +func diagnoseVariableDecl( + customSettings: VariableCustomSettings?, + variable: VariableDeclSyntax, + targetAccessLevel: AccessLevelModifier +) -> [Diagnostic] { + let customSettingsDiagnostics = + customSettings.map { settings in + [ + diagnoseVariableLabel(customSettings: settings, variable: variable), + diagnoseDefaultValueAppliedToMultipleBindings( + customSettings: settings, + variable: variable + ), + ].compactMap { $0 } + } ?? [] + + let accessibilityDiagnostics = [ + diagnoseAccessibilityLeak( + customSettings: customSettings, + variable: variable, + targetAccessLevel: targetAccessLevel + ) + ].compactMap { $0 } + + return customSettingsDiagnostics + accessibilityDiagnostics +} - case .tupleDestructuringInProperty: - ".tupleUsedInProperty" - } +private func diagnoseVariableLabel( + customSettings: VariableCustomSettings, + variable: VariableDeclSyntax +) -> Diagnostic? { + if let label = customSettings.label, + label != "_", + variable.bindings.count > 1 + { + return customSettings.diagnosticOnLabel( + MacroExpansionErrorMessage("Custom 'label' can't be applied to multiple bindings") + ) } - var severity: DiagnosticSeverity { .error } - - var message: String { - switch self { - case .defaultAppliedToMultipleBindings: - return "Custom 'default' can't be applied to multiple bindings" + if customSettings.label?.isInvalidSwiftLabel ?? false { + return customSettings.diagnosticOnLabelValue(MacroExpansionErrorMessage("Invalid label value")) + } - case .labelAppliedToMultipleBindings: - return """ - Custom 'label' can't be applied to multiple bindings - """ + return nil +} - case let .invalidDeclarationKind(declGroup): - return """ - @MemberwiseInit can only be attached to a struct, class, or actor; \ - not to \(declGroup.descriptiveDeclKind(withArticle: true)). - """ +private func diagnoseDefaultValueAppliedToMultipleBindings( + customSettings: VariableCustomSettings, + variable: VariableDeclSyntax +) -> Diagnostic? { + guard + customSettings.defaultValue != nil, + variable.bindings.count > 1 + else { return nil } + + return customSettings.diagnosticOnDefault( + MacroExpansionErrorMessage("Custom 'default' can't be applied to multiple bindings") + ) +} - case .invalidSwiftLabel: - return "Invalid label value" +private func diagnoseAccessibilityLeak( + customSettings: VariableCustomSettings?, + variable: VariableDeclSyntax, + targetAccessLevel: AccessLevelModifier +) -> Diagnostic? { + let effectiveAccessLevel = customSettings?.accessLevel ?? variable.accessLevel + + guard + targetAccessLevel > effectiveAccessLevel, + !variable.isFullyInitializedLet + else { return nil } + + let customAccess = variable.customConfigurationArguments? + .first? + .expression + .as(MemberAccessExprSyntax.self) + + let targetNode = + customAccess?._syntaxNode + ?? (variable.modifiers.isEmpty ? variable._syntaxNode : variable.modifiers._syntaxNode) + + return Diagnostic( + node: targetNode, + message: MacroExpansionErrorMessage( + """ + @MemberwiseInit(.\(targetAccessLevel)) would leak access to '\(effectiveAccessLevel)' property + """ + ) + ) +} - case let .labelConflictsWithProperty(label): - return "Label '\(label)' conflicts with a property name" +// MARK: - Diagnose [PropertyBinding] and [MemberProperty] - case let .labelConflictsWithAnotherLabel(label): - return "Label '\(label)' conflicts with another label" +func customInitLabelDiagnosticsFor(bindings: [PropertyBinding]) -> [Diagnostic] { + var diagnostics: [Diagnostic] = [] - case .missingTypeForVarProperty: - return - "@MemberwiseInit requires a type annotation." + let customLabeledBindings = bindings.filter { + $0.variable.customSettings?.label != nil + } - case .tupleDestructuringInProperty: - return """ - @MemberwiseInit does not support tuple destructuring for property declarations. \ - Use multiple declarations instead. - """ + // Diagnose custom label conflicts with another custom label + var seenCustomLabels: Set = [] + for binding in customLabeledBindings { + guard + let customSettings = binding.variable.customSettings, + let label = customSettings.label, + label != "_" + else { continue } + defer { seenCustomLabels.insert(label) } + if seenCustomLabels.contains(label) { + diagnostics.append( + customSettings.diagnosticOnLabelValue( + MacroExpansionErrorMessage("Label '\(label)' conflicts with another label") + ) + ) } } - var diagnosticID: MessageID { - .init(domain: "MemberwiseInitMacro", id: rawValue) - } + return diagnostics } -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: - return "`\(self.kind)`" - } +func customInitLabelDiagnosticsFor(properties: [MemberProperty]) -> [Diagnostic] { + var diagnostics: [Diagnostic] = [] + + let propertiesByName = Dictionary(uniqueKeysWithValues: properties.map { ($0.name, $0) }) + + // Diagnose custom label conflicts with a property + for property in properties { + guard + let propertyCustomSettings = property.customSettings, + let label = propertyCustomSettings.label, + let duplicated = propertiesByName[label], + duplicated != property + else { continue } + + diagnostics.append( + propertyCustomSettings.diagnosticOnLabelValue( + MacroExpansionErrorMessage("Label '\(label)' conflicts with a property name") + ) + ) } + + return diagnostics } diff --git a/Sources/MemberwiseInitMacros/Macros/Support/Models.swift b/Sources/MemberwiseInitMacros/Macros/Support/Models.swift new file mode 100644 index 0000000..c62569c --- /dev/null +++ b/Sources/MemberwiseInitMacros/Macros/Support/Models.swift @@ -0,0 +1,148 @@ +import SwiftDiagnostics +import SwiftSyntax + +struct VariableCustomSettings: Equatable { + enum Assignee: Equatable { + case wrapper + case raw(String) + } + + let accessLevel: AccessLevelModifier? + let assignee: Assignee? + let defaultValue: String? + let forceEscaping: Bool + let ignore: Bool + let label: String? + let type: TypeSyntax? + let _syntaxNode: AttributeSyntax + + func diagnosticOnDefault(_ message: DiagnosticMessage) -> Diagnostic { + let labelNode = self._syntaxNode + .arguments? + .as(LabeledExprListSyntax.self)? + .firstWhereLabel("default") + + return diagnostic(node: labelNode ?? self._syntaxNode, message: message) + } + + func diagnosticOnLabel(_ message: DiagnosticMessage) -> Diagnostic { + let labelNode = self._syntaxNode + .arguments? + .as(LabeledExprListSyntax.self)? + .firstWhereLabel("label") + + return diagnostic(node: labelNode ?? self._syntaxNode, message: message) + } + + func diagnosticOnLabelValue(_ message: DiagnosticMessage) -> Diagnostic { + let labelValueNode = self._syntaxNode + .arguments? + .as(LabeledExprListSyntax.self)? + .firstWhereLabel("label")? + .expression + + return diagnostic(node: labelValueNode ?? self._syntaxNode, message: message) + } + + private func diagnostic( + node: any SyntaxProtocol, + message: DiagnosticMessage + ) -> Diagnostic { + Diagnostic(node: node, message: message) + } +} + +struct PropertyBinding { + let typeFromTrailingBinding: TypeSyntax? + let syntax: PatternBindingSyntax + let variable: MemberVariable + + var effectiveType: TypeSyntax? { + variable.customSettings?.type + ?? self.syntax.typeAnnotation?.type + ?? self.syntax.initializer?.value.inferredTypeSyntax + ?? self.typeFromTrailingBinding + } + + var initializerValue: ExprSyntax? { + self.syntax.initializer?.trimmed.value + } + + var isTuplePattern: Bool { + self.syntax.pattern.isTuplePattern + } + + var name: String? { + self.syntax.pattern.as(IdentifierPatternSyntax.self)?.identifier.text + } + + var isInitializedVarWithoutType: Bool { + self.initializerValue != nil + && self.variable.keywordToken == .keyword(.var) + && self.effectiveType == nil + && self.initializerValue?.inferredTypeSyntax == nil + } + + var isInitializedLet: Bool { + self.initializerValue != nil && self.variable.keywordToken == .keyword(.let) + } + + func diagnostic(_ message: DiagnosticMessage) -> Diagnostic { + Diagnostic(node: self.syntax._syntaxNode, message: message) + } +} + +struct MemberVariable { + let customSettings: VariableCustomSettings? + let syntax: VariableDeclSyntax + + var accessLevel: AccessLevelModifier { + self.syntax.accessLevel + } + + var bindings: PatternBindingListSyntax { + self.syntax.bindings + } + + var keywordToken: TokenKind { + self.syntax.bindingSpecifier.tokenKind + } +} + +struct MemberProperty: Equatable { + let accessLevel: AccessLevelModifier + let customSettings: VariableCustomSettings? + let initializerValue: ExprSyntax? + let keywordToken: TokenKind + let name: String + let type: TypeSyntax + + func initParameterLabel( + considering allProperties: [MemberProperty], + deunderscoreParameters: Bool + ) -> String { + guard + let customSettings = self.customSettings, + customSettings.label + != self.initParameterName( + considering: allProperties, + deunderscoreParameters: deunderscoreParameters + ) + else { return "" } + + return customSettings.label.map { "\($0) " } ?? "" + } + + func initParameterName( + considering allProperties: [MemberProperty], + deunderscoreParameters: Bool + ) -> String { + guard + self.customSettings?.label == nil, + deunderscoreParameters + else { return self.name } + + let potentialName = self.name.hasPrefix("_") ? String(name.dropFirst()) : self.name + return allProperties.contains(where: { $0.name == potentialName }) ? self.name : potentialName + } +} diff --git a/Sources/MemberwiseInitMacros/Macros/Support/SyntaxHelpers.swift b/Sources/MemberwiseInitMacros/Macros/Support/SyntaxHelpers.swift index d63d8a1..aa46cb0 100644 --- a/Sources/MemberwiseInitMacros/Macros/Support/SyntaxHelpers.swift +++ b/Sources/MemberwiseInitMacros/Macros/Support/SyntaxHelpers.swift @@ -84,3 +84,24 @@ extension ExprSyntax { .trimmingCharacters(in: .whitespacesAndNewlines) } } + +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: + return "`\(self.kind)`" + } + } +} diff --git a/Tests/MemberwiseInitTests/LayeredDiagnosticsTests.swift b/Tests/MemberwiseInitTests/LayeredDiagnosticsTests.swift new file mode 100644 index 0000000..6ed4c75 --- /dev/null +++ b/Tests/MemberwiseInitTests/LayeredDiagnosticsTests.swift @@ -0,0 +1,132 @@ +import MacroTesting +import MemberwiseInitMacros +import XCTest + +final class LayeredDiagnosticsTests: XCTestCase { + override func invokeTest() { + // NB: Waiting for swift-macro-testing PR to support explicit indentationWidth: https://github.com/pointfreeco/swift-macro-testing/pull/8 + withMacroTesting( + //indentationWidth: .spaces(2), + macros: [ + "MemberwiseInit": MemberwiseInitMacro.self, + "InitRaw": InitMacro.self, + ] + ) { + super.invokeTest() + } + } + + func testInvalidLabelOnMultipleBindings() { + assertMacro { + """ + @MemberwiseInit + struct S { + @Init(label: "$foo") let x, y: T + } + """ + } diagnostics: { + """ + @MemberwiseInit + struct S { + @Init(label: "$foo") let x, y: T + ┬──────────── + ╰─ 🛑 Custom 'label' can't be applied to multiple bindings + } + """ + } + } + + func testInvalidLabelAndDefaultOnMultipleBindings() { + assertMacro { + """ + @MemberwiseInit + struct S { + @Init(default: 0, label: "$foo") let x, y: T + } + """ + } diagnostics: { + """ + @MemberwiseInit + struct S { + @Init(default: 0, label: "$foo") let x, y: T + ┬──────────── + │ ╰─ 🛑 Custom 'label' can't be applied to multiple bindings + ┬────────── + ╰─ 🛑 Custom 'default' can't be applied to multiple bindings + } + """ + } + } + + func testAccessLeakCustomDefaultAndInvalidLabelOnMultipleBindings() { + assertMacro { + """ + @MemberwiseInit(.public) + struct S { + @Init(default: 0, label: "$foo") private let x, y: T + } + """ + } diagnostics: { + """ + @MemberwiseInit(.public) + struct S { + @Init(default: 0, label: "$foo") private let x, y: T + ┬────── + │ │ ╰─ 🛑 @MemberwiseInit(.public) would leak access to 'private' property + ┬──────────── + │ ╰─ 🛑 Custom 'label' can't be applied to multiple bindings + ┬────────── + ╰─ 🛑 Custom 'default' can't be applied to multiple bindings + } + """ + } + } + + func testAccessLeakAndCustomLabelConflictsWithAnotherLabel() { + assertMacro { + """ + @MemberwiseInit(.public) + struct S { + @Init(label: "foo") let x: T + @Init(label: "foo") let y: T + } + """ + } diagnostics: { + """ + @MemberwiseInit(.public) + struct S { + @Init(label: "foo") let x: T + ┬─────────────────────────── + ╰─ 🛑 @MemberwiseInit(.public) would leak access to 'internal' property + @Init(label: "foo") let y: T + ┬─────────────────────────── + ╰─ 🛑 @MemberwiseInit(.public) would leak access to 'internal' property + } + """ + } + } + + func testAccessLeakAndCustomLabelConflictsWithPropertyName() { + assertMacro { + """ + @MemberwiseInit(.public) + struct S { + @Init(label: "y") let x: T + let y: T + } + """ + } diagnostics: { + """ + @MemberwiseInit(.public) + struct S { + @Init(label: "y") let x: T + ┬───────────────────────── + ╰─ 🛑 @MemberwiseInit(.public) would leak access to 'internal' property + let y: T + ┬─────── + ╰─ 🛑 @MemberwiseInit(.public) would leak access to 'internal' property + } + """ + } + } +}