From 403afe97dcdaa8cedc8de7dfe581c33cbecc5462 Mon Sep 17 00:00:00 2001 From: MahdiBM Date: Tue, 16 Jul 2024 20:17:07 +0330 Subject: [PATCH] add diagnosis for unavailable function usage, when possible --- Package.resolved | 8 ++--- Package.swift | 4 +-- .../EnumeratorMacroType.swift | 24 +++++++++++---- Sources/EnumeratorMacroImpl/MacroError.swift | 8 +++++ .../RenderingContext.swift | 14 +++++++++ .../EnumeratorMacroImpl/Types/EArray.swift | 13 ++++++++ Sources/EnumeratorMacroImpl/Types/ECase.swift | 6 ++++ .../EnumeratorMacroImpl/Types/ECases.swift | 12 ++++++++ .../EnumeratorMacroImpl/Types/EKeyValue.swift | 12 ++++++++ .../EnumeratorMacroImpl/Types/EOptional.swift | 30 ++++++++++++++++++- .../Types/EOptionalsArray.swift | 13 ++++++++ .../Types/EParameter.swift | 6 ++++ .../Types/EParameters.swift | 13 ++++++++ .../EnumeratorMacroImpl/Types/EString.swift | 13 ++++++++ .../Types/WithNormalizedTypeName.swift | 13 ++++++++ 15 files changed, 177 insertions(+), 12 deletions(-) create mode 100644 Sources/EnumeratorMacroImpl/RenderingContext.swift create mode 100644 Sources/EnumeratorMacroImpl/Types/WithNormalizedTypeName.swift diff --git a/Package.resolved b/Package.resolved index a6e03d7..ce231a6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "db43e6862c0523c2f43b5b63513da705ad591422a0926a66ee8217e5062d2710", + "originHash" : "5fb3f3f652f6046ecafacd4cf94fd7cbc6347977cdfde61d767ba9728638b23b", "pins" : [ { "identity" : "swift-mustache", "kind" : "remoteSourceControl", - "location" : "https://github.com/mahdibm/swift-mustache", + "location" : "https://github.com/hummingbird-project/swift-mustache", "state" : { - "branch" : "mmbm-swift-6", - "revision" : "b3cd0bca48cd1883e52a1ed6b79dfccd64ff5952" + "revision" : "5bb66ac425ec25fd45e0cd7d3829d73e45096352", + "version" : "2.0.0-beta.2" } }, { diff --git a/Package.swift b/Package.swift index 223b144..f09a83f 100644 --- a/Package.swift +++ b/Package.swift @@ -24,8 +24,8 @@ let package = Package( "510.0.0" ..< "610.0.0" ), .package( - url: "https://github.com/mahdibm/swift-mustache", - branch: "mmbm-swift-6" + url: "https://github.com/hummingbird-project/swift-mustache", + from: "2.0.0-beta.2" ), ], targets: [ diff --git a/Sources/EnumeratorMacroImpl/EnumeratorMacroType.swift b/Sources/EnumeratorMacroImpl/EnumeratorMacroType.swift index 43c78f8..4dd80e9 100644 --- a/Sources/EnumeratorMacroImpl/EnumeratorMacroType.swift +++ b/Sources/EnumeratorMacroImpl/EnumeratorMacroType.swift @@ -48,11 +48,25 @@ extension EnumeratorMacroType: MemberMacro { let rendered = templates.compactMap { (template, syntax) -> (rendered: String, syntax: StringLiteralExprSyntax)? in do { - let rendered = try MustacheTemplate( - string: "{{%CONTENT_TYPE:TEXT}}\n" + template - ).render([ - "cases": cases - ]) + let rendered: String? = try RenderingContext.$current.withValue(.init()) { + let result = try MustacheTemplate( + string: "{{%CONTENT_TYPE:TEXT}}\n" + template + ).render([ + "cases": cases + ]) + if let diagnostic = RenderingContext.current.diagnostic { + context.addDiagnostics( + from: diagnostic, + node: syntax + ) + return nil + } else { + return result + } + } + guard let rendered else { + return nil + } return (rendered, syntax) } catch { let message: MacroError diff --git a/Sources/EnumeratorMacroImpl/MacroError.swift b/Sources/EnumeratorMacroImpl/MacroError.swift index 5ac3baa..9d9d6a9 100644 --- a/Sources/EnumeratorMacroImpl/MacroError.swift +++ b/Sources/EnumeratorMacroImpl/MacroError.swift @@ -11,6 +11,7 @@ enum MacroError: Error, CustomStringConvertible { case couldNotFindLocationOfNode(syntax: String) case mustacheTemplateError(message: String) case internalError(String) + case invalidTransform(transform: String, normalizedTypeName: String) var caseName: String { switch self { @@ -32,6 +33,8 @@ enum MacroError: Error, CustomStringConvertible { "mustacheTemplateError" case .internalError: "internalError" + case .invalidTransform: + "invalidTransform" } } @@ -55,6 +58,11 @@ enum MacroError: Error, CustomStringConvertible { "Error while rendering the template: \(message)" case let .internalError(message): "An internal error occurred. Please file a bug report at https://github.com/mahdibm/enumerator-macro. Error:\n\(message)" + case let .invalidTransform(transform, normalizedTypeName): + """ + Invalid function call detected. + '\(normalizedTypeName)' doesn't have a function called '\(transform)' + """ } } } diff --git a/Sources/EnumeratorMacroImpl/RenderingContext.swift b/Sources/EnumeratorMacroImpl/RenderingContext.swift new file mode 100644 index 0000000..4bf4919 --- /dev/null +++ b/Sources/EnumeratorMacroImpl/RenderingContext.swift @@ -0,0 +1,14 @@ +/// The macro works in a single thread so `@unchecked Sendable` is justified. +final class RenderingContext: @unchecked Sendable { + @TaskLocal static var current: RenderingContext! + + var diagnostic: MacroError? + + func cleanDiagnostic() { + self.diagnostic = nil + } + + func addOrReplaceDiagnostic(_ error: MacroError) { + self.diagnostic = error + } +} diff --git a/Sources/EnumeratorMacroImpl/Types/EArray.swift b/Sources/EnumeratorMacroImpl/Types/EArray.swift index 0e009ab..12153c9 100644 --- a/Sources/EnumeratorMacroImpl/Types/EArray.swift +++ b/Sources/EnumeratorMacroImpl/Types/EArray.swift @@ -26,6 +26,12 @@ extension EArray: CustomStringConvertible { } } +extension EArray: WithNormalizedTypeName { + static var normalizedTypeName: String { + "[\(bestEffortTypeName(Element.self))]" + } +} + extension EArray: CustomReflectable { var customMirror: Mirror { Mirror(reflecting: self.underlying) @@ -37,6 +43,7 @@ extension EArray: MustacheTransformable { if let defaultTransformed = self.underlying.transform(name) { return convertToCustomTypesIfPossible(defaultTransformed) } else { + RenderingContext.current.cleanDiagnostic() switch name { case "joined": let joined = self.underlying @@ -68,6 +75,12 @@ extension EArray: MustacheTransformable { let value = keyValues.first(where: { $0.key == name })?.value return EOptional(value) } + RenderingContext.current.addOrReplaceDiagnostic( + .invalidTransform( + transform: name, + normalizedTypeName: Self.normalizedTypeName + ) + ) return nil } } diff --git a/Sources/EnumeratorMacroImpl/Types/ECase.swift b/Sources/EnumeratorMacroImpl/Types/ECase.swift index 9c99bb3..de63930 100644 --- a/Sources/EnumeratorMacroImpl/Types/ECase.swift +++ b/Sources/EnumeratorMacroImpl/Types/ECase.swift @@ -24,3 +24,9 @@ struct ECase { self.comments = .init(underlying: keyValueParts.map(EString.init)) } } + +extension ECase: WithNormalizedTypeName { + static var normalizedTypeName: String { + "Case" + } +} diff --git a/Sources/EnumeratorMacroImpl/Types/ECases.swift b/Sources/EnumeratorMacroImpl/Types/ECases.swift index e20a8bd..4c24e50 100644 --- a/Sources/EnumeratorMacroImpl/Types/ECases.swift +++ b/Sources/EnumeratorMacroImpl/Types/ECases.swift @@ -18,6 +18,11 @@ extension ECases: CustomStringConvertible { self.underlying.description } } +extension ECases: WithNormalizedTypeName { + static var normalizedTypeName: String { + "[Case]" + } +} extension ECases: Sequence, MustacheSequence { func makeIterator() -> Array.Iterator { @@ -36,12 +41,19 @@ extension ECases: MustacheTransformable { if let defaultTransformed = self.underlying.transform(name) { return convertToCustomTypesIfPossible(defaultTransformed) } else { + RenderingContext.current.cleanDiagnostic() switch name { case "filterNoParams": return self.filter(\.parameters.underlying.underlying.isEmpty) case "filterWithParams": return self.filter({ !$0.parameters.underlying.underlying.isEmpty }) default: + RenderingContext.current.addOrReplaceDiagnostic( + .invalidTransform( + transform: name, + normalizedTypeName: Self.normalizedTypeName + ) + ) return nil } } diff --git a/Sources/EnumeratorMacroImpl/Types/EKeyValue.swift b/Sources/EnumeratorMacroImpl/Types/EKeyValue.swift index 6706871..06efe20 100644 --- a/Sources/EnumeratorMacroImpl/Types/EKeyValue.swift +++ b/Sources/EnumeratorMacroImpl/Types/EKeyValue.swift @@ -16,6 +16,12 @@ extension EKeyValue: CustomStringConvertible { } } +extension EKeyValue: WithNormalizedTypeName { + static var normalizedTypeName: String { + "KeyValue" + } +} + extension EKeyValue: MustacheTransformable { func transform(_ name: String) -> Any? { switch name { @@ -24,6 +30,12 @@ extension EKeyValue: MustacheTransformable { case "value": return self.value default: + RenderingContext.current.addOrReplaceDiagnostic( + .invalidTransform( + transform: name, + normalizedTypeName: Self.normalizedTypeName + ) + ) return nil } } diff --git a/Sources/EnumeratorMacroImpl/Types/EOptional.swift b/Sources/EnumeratorMacroImpl/Types/EOptional.swift index d6586e4..65157ec 100644 --- a/Sources/EnumeratorMacroImpl/Types/EOptional.swift +++ b/Sources/EnumeratorMacroImpl/Types/EOptional.swift @@ -69,6 +69,12 @@ extension EOptional: CustomStringConvertible { } } +extension EOptional: WithNormalizedTypeName { + static var normalizedTypeName: String { + "Optional<\(bestEffortTypeName(Wrapped.self))>" + } +} + extension EOptional: MustacheTransformable { func transform(_ name: String) -> Any? { switch self { @@ -79,6 +85,12 @@ extension EOptional: MustacheTransformable { case "exists": return false default: + RenderingContext.current.addOrReplaceDiagnostic( + .invalidTransform( + transform: name, + normalizedTypeName: Self.normalizedTypeName + ) + ) return nil } case let .some(value): @@ -87,8 +99,24 @@ extension EOptional: MustacheTransformable { return true default: if let value = value as? MustacheTransformable { - return value.transform(name) + if let transformed = value.transform(name) { + return transformed + } else { + RenderingContext.current.addOrReplaceDiagnostic( + .invalidTransform( + transform: name, + normalizedTypeName: bestEffortTypeName(Wrapped.self) + ) + ) + return nil + } } else { + RenderingContext.current.addOrReplaceDiagnostic( + .invalidTransform( + transform: name, + normalizedTypeName: Self.normalizedTypeName + ) + ) return nil } } diff --git a/Sources/EnumeratorMacroImpl/Types/EOptionalsArray.swift b/Sources/EnumeratorMacroImpl/Types/EOptionalsArray.swift index 8c36410..d0ef2e1 100644 --- a/Sources/EnumeratorMacroImpl/Types/EOptionalsArray.swift +++ b/Sources/EnumeratorMacroImpl/Types/EOptionalsArray.swift @@ -29,6 +29,12 @@ extension EOptionalsArray: CustomStringConvertible { } } +extension EOptionalsArray: WithNormalizedTypeName { + static var normalizedTypeName: String { + "[Optional<\(bestEffortTypeName(Element.self))>]" + } +} + extension EOptionalsArray: CustomReflectable { var customMirror: Mirror { Mirror(reflecting: self.underlying) @@ -40,6 +46,7 @@ extension EOptionalsArray: MustacheTransformable { if let defaultTransformed = self.underlying.transform(name) { return convertToCustomTypesIfPossible(defaultTransformed) } else { + RenderingContext.current.cleanDiagnostic() switch name { case "joined": let joined = self.underlying @@ -68,6 +75,12 @@ extension EOptionalsArray: MustacheTransformable { } return EArray(underlying: split) default: + RenderingContext.current.addOrReplaceDiagnostic( + .invalidTransform( + transform: name, + normalizedTypeName: Self.normalizedTypeName + ) + ) return nil } } diff --git a/Sources/EnumeratorMacroImpl/Types/EParameter.swift b/Sources/EnumeratorMacroImpl/Types/EParameter.swift index b91ed54..9b3010c 100644 --- a/Sources/EnumeratorMacroImpl/Types/EParameter.swift +++ b/Sources/EnumeratorMacroImpl/Types/EParameter.swift @@ -19,6 +19,12 @@ struct EParameter { } } +extension EParameter: WithNormalizedTypeName { + static var normalizedTypeName: String { + "Parameter" + } +} + private extension TypeSyntax { var isOptional: Bool { switch self.kind { diff --git a/Sources/EnumeratorMacroImpl/Types/EParameters.swift b/Sources/EnumeratorMacroImpl/Types/EParameters.swift index 8eaf79a..1191b05 100644 --- a/Sources/EnumeratorMacroImpl/Types/EParameters.swift +++ b/Sources/EnumeratorMacroImpl/Types/EParameters.swift @@ -14,6 +14,12 @@ extension EParameters: CustomStringConvertible { } } +extension EParameters: WithNormalizedTypeName { + static var normalizedTypeName: String { + "[Parameter]" + } +} + extension EParameters: Sequence, MustacheSequence { func makeIterator() -> Array.Iterator { self.underlying.makeIterator() @@ -31,6 +37,7 @@ extension EParameters: MustacheTransformable { if let defaultTransformed = self.underlying.transform(name) { return convertToCustomTypesIfPossible(defaultTransformed) } else { + RenderingContext.current.cleanDiagnostic() switch name { case "names": let names = self @@ -69,6 +76,12 @@ extension EParameters: MustacheTransformable { return array } default: + RenderingContext.current.addOrReplaceDiagnostic( + .invalidTransform( + transform: name, + normalizedTypeName: Self.normalizedTypeName + ) + ) return nil } } diff --git a/Sources/EnumeratorMacroImpl/Types/EString.swift b/Sources/EnumeratorMacroImpl/Types/EString.swift index 2b9d5d6..b9904db 100644 --- a/Sources/EnumeratorMacroImpl/Types/EString.swift +++ b/Sources/EnumeratorMacroImpl/Types/EString.swift @@ -14,6 +14,12 @@ extension EString: CustomStringConvertible { } } +extension EString: WithNormalizedTypeName { + static var normalizedTypeName: String { + "String" + } +} + extension EString: CustomReflectable { var customMirror: Mirror { self.underlying.customMirror @@ -25,6 +31,7 @@ extension EString: MustacheTransformable { if let defaultTransformed = self.underlying.transform(name) { return convertToCustomTypesIfPossible(defaultTransformed) } else { + RenderingContext.current.cleanDiagnostic() switch name { case "firstCapitalized": if self.isEmpty || self[self.startIndex].isUppercase { @@ -60,6 +67,12 @@ extension EString: MustacheTransformable { value: EString(split.count > 1 ? split[1] : "") ) default: + RenderingContext.current.addOrReplaceDiagnostic( + .invalidTransform( + transform: name, + normalizedTypeName: Self.normalizedTypeName + ) + ) return nil } } diff --git a/Sources/EnumeratorMacroImpl/Types/WithNormalizedTypeName.swift b/Sources/EnumeratorMacroImpl/Types/WithNormalizedTypeName.swift new file mode 100644 index 0000000..d3bcd26 --- /dev/null +++ b/Sources/EnumeratorMacroImpl/Types/WithNormalizedTypeName.swift @@ -0,0 +1,13 @@ + +protocol WithNormalizedTypeName { + static var normalizedTypeName: String { get } +} + +func bestEffortTypeName(_ type: T.Type = T.self) -> String { + switch type { + case let customType as WithNormalizedTypeName.Type: + customType.normalizedTypeName + default: + Swift._typeName(type, qualified: false) + } +}