From f6b4438860d7d255c3fcbb0a32dee5ca0fd4aca6 Mon Sep 17 00:00:00 2001 From: MahdiBM Date: Wed, 24 Jul 2024 01:07:33 +0330 Subject: [PATCH] automatically apply FixIts --- Package.resolved | 6 +- Package.swift | 4 +- Package@swift-6.0.swift | 4 +- Sources/EnumeratorMacro/Enumerator.swift | 138 ------------------ .../EnumeratorMacroType.swift | 107 +++++++++----- .../ExcessiveTriviaRemover.swift | 0 .../Visitors/PlacehodlerDetector.swift | 17 +++ .../SwitchErrorsRewriter.swift | 0 .../SwitchWarningsRewriter.swift | 0 .../EnumeratorMacroTests.swift | 55 ++++--- 10 files changed, 123 insertions(+), 208 deletions(-) rename Sources/EnumeratorMacroImpl/{SyntaxRewriters => Visitors}/ExcessiveTriviaRemover.swift (100%) create mode 100644 Sources/EnumeratorMacroImpl/Visitors/PlacehodlerDetector.swift rename Sources/EnumeratorMacroImpl/{SyntaxRewriters => Visitors}/SwitchErrorsRewriter.swift (100%) rename Sources/EnumeratorMacroImpl/{SyntaxRewriters => Visitors}/SwitchWarningsRewriter.swift (100%) diff --git a/Package.resolved b/Package.resolved index f346b2c..3391ef0 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "471e59eb3ce1f697200ce8e807d75c97a2b2065d8033fd5c430fc887e3c33f05", + "originHash" : "6014e531cd0700b62246ca0fa1188acb8e5c483aab4374ff903c74b6c9cfa34b", "pins" : [ { "identity" : "swift-mustache", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "303e5c5c36d6a558407d364878df131c3546fad8", - "version" : "510.0.2" + "revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c", + "version" : "600.0.0-prerelease-2024-06-12" } } ], diff --git a/Package.swift b/Package.swift index 375e040..d92d926 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/swiftlang/swift-syntax", - "510.0.0" ..< "610.0.0" + from: "510.0.0" ), .package( url: "https://github.com/hummingbird-project/swift-mustache", @@ -34,6 +34,7 @@ let package = Package( dependencies: [ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), + .product(name: "SwiftIDEUtils", package: "swift-syntax"), .product(name: "Mustache", package: "swift-mustache"), ], swiftSettings: swiftSettings @@ -52,6 +53,7 @@ let package = Package( .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftIDEUtils", package: "swift-syntax"), .product(name: "Mustache", package: "swift-mustache"), ], swiftSettings: swiftSettings diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 2972b2c..3ff95fa 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -21,7 +21,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/swiftlang/swift-syntax", - "510.0.0" ..< "610.0.0" + .upToNextMinor(from: "600.0.0-prerelease-2024-06-12") ), .package( url: "https://github.com/hummingbird-project/swift-mustache", @@ -34,6 +34,7 @@ let package = Package( dependencies: [ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), + .product(name: "SwiftIDEUtils", package: "swift-syntax"), .product(name: "Mustache", package: "swift-mustache"), ], swiftSettings: swiftSettings @@ -52,6 +53,7 @@ let package = Package( .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftIDEUtils", package: "swift-syntax"), .product(name: "Mustache", package: "swift-mustache"), ], swiftSettings: swiftSettings diff --git a/Sources/EnumeratorMacro/Enumerator.swift b/Sources/EnumeratorMacro/Enumerator.swift index 42116c5..8fee2ae 100644 --- a/Sources/EnumeratorMacro/Enumerator.swift +++ b/Sources/EnumeratorMacro/Enumerator.swift @@ -6,141 +6,3 @@ public macro Enumerator( module: "EnumeratorMacroImpl", type: "EnumeratorMacroType" ) - -@Enumerator(""" -var caseName: String { - switch self { - {{#cases}} - case .{{name}}: "{{#first(parameters)}} {{name}} {{/first(parameters)}}" - {{/cases}} - } -} -""") -enum TestEnum { - case a(val1: String, val2: Int) - case b - case testCase(testValue: String) -} - -public protocol LocalizationServiceProtocol { - static func localizedString(language: String, term: String, parameters: Any...) -> String -} - -package enum SharedConfiguration { - package enum Env: String { - case local - case testing - case prod - } - - package static var env: Env { fatalError() } -} - -@Enumerator(allowedComments: ["business_error", "l8n_params"], -""" -public enum Subtype: String, Equatable { - {{#cases}} - case {{name}} - {{/cases}} -} -""", -""" -public var subtype: Subtype { - switch self { - {{#cases}} - case .{{name}}: - .{{name}} - {{/cases}} - } -} -""", -""" -public var errorCode: String { - switch self { - {{#cases}} - case .{{name}}: - "ERROR-{{plusOne(index)}}" - {{/cases}} - } -} -""", -""" -public var loggerMetadata: [String: String] { - switch self { - {{#cases}} {{^isEmpty(parameters)}} - case let .{{name}}{{withParens(joined(names(parameters)))}}: - [ - "caseName": self.caseName, - {{#names(parameters)}} - "case_{{.}}": String(reflecting: {{.}}), - {{/names(parameters)}} - ] - {{/isEmpty(parameters)}} {{/cases}} - default: - ["caseName": self.caseName] - } -} -""", -""" -private var localizationParameters: [Any] { - switch self { - {{#cases}} {{^isEmpty(parameters)}} - - {{^isEmpty(l8n_params(comments))}} - case let .{{name}}{{withParens(joined(names(parameters)))}}: - [{{l8n_params(comments)}}] - {{/isEmpty(l8n_params(comments))}} - - {{^exists(l8n_params(comments))}} - case let .{{name}}{{withParens(joined(names(parameters)))}}: - [ - {{#parameters}} - {{name}}{{#isOptional}} as Any{{/isOptional}}, - {{/parameters}} - ] - {{/exists(l8n_params(comments))}} - - {{/isEmpty(parameters)}} {{/cases}} - default: - [] - } -} -""") -@Enumerator( - allowedComments: ["business_error", "l8n_params"], - #""" - package var isBusinessLogicError: Bool { - switch self { - case - {{#cases}}{{#bool(business_error(comments))}} - .{{name}}, - {{/bool(business_error(comments))}}{{/cases}} - : - return true - default: - return false - } - } - """# -) -public enum ErrorMessage { - public static let localizationServiceType: LocalizationServiceProtocol.Type? = nil - - case allergenAlreadyAdded // business_error - case alreadyOngoingInventory - case apiKeyWithoutEnoughPermission(integration: String, other: Bool?, Int) - case databaseError(error: Error, isConstraintViolation: Bool) // business_error; l8n_params: - - public var caseName: String { - self.subtype.rawValue - } - - public func toString(_ language: String) -> String { - let translation = Self.localizationServiceType?.localizedString( - language: language, - term: "api.\(self.caseName)", - parameters: self.localizationParameters - ) - return translation ?? (SharedConfiguration.env == .testing ? String(reflecting: self) : "") - } -} diff --git a/Sources/EnumeratorMacroImpl/EnumeratorMacroType.swift b/Sources/EnumeratorMacroImpl/EnumeratorMacroType.swift index de24d59..5b66d14 100644 --- a/Sources/EnumeratorMacroImpl/EnumeratorMacroType.swift +++ b/Sources/EnumeratorMacroImpl/EnumeratorMacroType.swift @@ -1,3 +1,4 @@ +@_spi(FixItApplier) import SwiftIDEUtils import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacros @@ -113,51 +114,83 @@ extension EnumeratorMacroType: MemberMacro { from: &parser ).statements.compactMap { statement -> DeclSyntax? in var statement = statement - var diagnostics = ParseDiagnosticsGenerator.diagnostics(for: statement) - if diagnostics.containsError { - /// Try to recover from errors: + + /// Returns if anything changed at all. + func tryApplyFixIts() -> Bool { + guard diagnostics.contains(where: { !$0.fixIts.isEmpty }) else { + return false + } + let fixedStatement = FixItApplier.applyFixes( + from: diagnostics, + filterByMessages: nil, + to: statement + ) + var parser = Parser(fixedStatement) + let newStatement = CodeBlockItemSyntax.parse(from: &parser) + guard statement != newStatement else { + return false + } + let placeholderDetector = PlaceholderDetector() + placeholderDetector.walk(newStatement) + /// One of the FixIts added a placeholder, so the fixes are unacceptable + /// Known behavior which is fine for now: even if one FixIt is + /// misbehaving, still none of the FixIts will be applied. + if placeholderDetector.containedPlaceholder { + return false + } else { + statement = newStatement + return true + } + } + + /// Returns if anything changed at all. + func tryManuallyFixErrors() -> Bool { let switchRewriter = SwitchErrorsRewriter() let fixedStatement = switchRewriter.rewrite(statement) - let newDiagnostics = ParseDiagnosticsGenerator.diagnostics(for: fixedStatement) - if !newDiagnostics.containsError { - switch CodeBlockItemSyntax(fixedStatement) { - case let .some(fixedStatement): - statement = fixedStatement - diagnostics = newDiagnostics - case .none: - context.diagnose( - Diagnostic( - node: codeSyntax, - message: MacroError.internalError( - "Could not convert a Syntax to a CodeBlockItemSyntax" - ) + switch CodeBlockItemSyntax(fixedStatement) { + case let .some(fixedStatement): + statement = fixedStatement + return true + case .none: + context.diagnose( + Diagnostic( + node: codeSyntax, + message: MacroError.internalError( + "Could not convert a Syntax to a CodeBlockItemSyntax" ) ) - return nil - } - } else { - /// If not recovered, throw a diagnostic error. - context.diagnose(.init( - node: codeSyntax, - message: MacroError.renderedSyntaxContainsErrors(statement.description) - )) + ) + return false } } - for diagnostic in diagnostics { - if diagnostic.diagMessage.severity == .error { - context.diagnose(.init( - node: codeSyntax, - position: diagnostic.position, - message: diagnostic.diagMessage, - highlights: diagnostic.highlights, - notes: diagnostic.notes, - fixIts: diagnostic.fixIts - )) - } else if let /*fixIt*/_ = diagnostic.fixIts.first { - /// TODO: Apply the fixit - } + if tryApplyFixIts() { + diagnostics = ParseDiagnosticsGenerator.diagnostics(for: statement) + } + + if diagnostics.containsError, tryManuallyFixErrors() { + diagnostics = ParseDiagnosticsGenerator.diagnostics(for: statement) + } + + if diagnostics.containsError { + /// If still not recovered, throw a diagnostic error. + context.diagnose(.init( + node: codeSyntax, + message: MacroError.renderedSyntaxContainsErrors(statement.description) + )) + } + + for diagnostic in diagnostics + where diagnostic.diagMessage.severity == .error { + context.diagnose(.init( + node: codeSyntax, + position: diagnostic.position, + message: diagnostic.diagMessage, + highlights: diagnostic.highlights, + notes: diagnostic.notes, + fixIts: diagnostic.fixIts + )) } if diagnostics.containsError { return nil diff --git a/Sources/EnumeratorMacroImpl/SyntaxRewriters/ExcessiveTriviaRemover.swift b/Sources/EnumeratorMacroImpl/Visitors/ExcessiveTriviaRemover.swift similarity index 100% rename from Sources/EnumeratorMacroImpl/SyntaxRewriters/ExcessiveTriviaRemover.swift rename to Sources/EnumeratorMacroImpl/Visitors/ExcessiveTriviaRemover.swift diff --git a/Sources/EnumeratorMacroImpl/Visitors/PlacehodlerDetector.swift b/Sources/EnumeratorMacroImpl/Visitors/PlacehodlerDetector.swift new file mode 100644 index 0000000..6109f51 --- /dev/null +++ b/Sources/EnumeratorMacroImpl/Visitors/PlacehodlerDetector.swift @@ -0,0 +1,17 @@ +import SwiftSyntax + +final class PlaceholderDetector: SyntaxVisitor { + var containedPlaceholder = false + + override init(viewMode: SyntaxTreeViewMode = .sourceAccurate) { + super.init(viewMode: viewMode) + } + + override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind { + if node.isEditorPlaceholder { + self.containedPlaceholder = true + return .skipChildren + } + return .visitChildren + } +} diff --git a/Sources/EnumeratorMacroImpl/SyntaxRewriters/SwitchErrorsRewriter.swift b/Sources/EnumeratorMacroImpl/Visitors/SwitchErrorsRewriter.swift similarity index 100% rename from Sources/EnumeratorMacroImpl/SyntaxRewriters/SwitchErrorsRewriter.swift rename to Sources/EnumeratorMacroImpl/Visitors/SwitchErrorsRewriter.swift diff --git a/Sources/EnumeratorMacroImpl/SyntaxRewriters/SwitchWarningsRewriter.swift b/Sources/EnumeratorMacroImpl/Visitors/SwitchWarningsRewriter.swift similarity index 100% rename from Sources/EnumeratorMacroImpl/SyntaxRewriters/SwitchWarningsRewriter.swift rename to Sources/EnumeratorMacroImpl/Visitors/SwitchWarningsRewriter.swift diff --git a/Tests/EnumeratorMacroTests/EnumeratorMacroTests.swift b/Tests/EnumeratorMacroTests/EnumeratorMacroTests.swift index 0279015..8902f71 100644 --- a/Tests/EnumeratorMacroTests/EnumeratorMacroTests.swift +++ b/Tests/EnumeratorMacroTests/EnumeratorMacroTests.swift @@ -1150,34 +1150,33 @@ final class EnumeratorMacroTests: XCTestCase { ) } -// func testAppliesFixIts() { -// assertMacroExpansion( -// #""" -// @Enumerator(""" -// var value: Int { -// a 1️⃣\u{a0}+ 2 -// } -// """) -// enum TestEnum { -// case a(val1: String, Int) -// case b -// case testCase(testValue: String) -// } -// """#, -// expandedSource: #""" -// enum TestEnum { -// case a(val1: String, Int) -// case b -// case testCase(testValue: String) -// -// var value: Int { -// a + 2 -// } -// } -// """#, -// macros: EnumeratorMacroEntryPoint.macros -// ) -// } + func testAppliesFixIts() { + let unterminatedString = """ + let unterminated = "This is unterminated + """ + assertMacroExpansion( + #""" + @Enumerator(""" + \#(unterminatedString) + """) + enum TestEnum { + case a(val1: String, Int) + case b + case testCase(testValue: String) + } + """#, + expandedSource: #""" + enum TestEnum { + case a(val1: String, Int) + case b + case testCase(testValue: String) + + let unterminated = "This is unterminated" + } + """#, + macros: EnumeratorMacroEntryPoint.macros + ) + } } @Enumerator("""