diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Graphaello.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Graphaello.xcscheme index 6423813..6397964 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Graphaello.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Graphaello.xcscheme @@ -52,14 +52,14 @@ diff --git a/Sources/Graphaello/Commands/Codegen/CodegenCommand.swift b/Sources/Graphaello/Commands/Codegen/CodegenCommand.swift index c6ed18c..542cd5b 100644 --- a/Sources/Graphaello/Commands/Codegen/CodegenCommand.swift +++ b/Sources/Graphaello/Commands/Codegen/CodegenCommand.swift @@ -48,6 +48,12 @@ class CodegenCommand : Command { Console.print(title: "๐Ÿ“š Parsing Paths From Structs:") let parsed = try pipeline.parse(extracted: extracted) + + let warnings = try pipeline.diagnose(parsed: parsed) + warnings.forEach { warning in + print(warning.description) + } + Console.print(result: "Found \(parsed.structs.count) structs with values from GraphQL") parsed.structs.forEach { parsed in Console.print(result: "\(inverse: parsed.name)", indentation: 2) @@ -70,46 +76,51 @@ class CodegenCommand : Command { cache[.lastRunHash] = hashValue } - Console.print(title: "๐Ÿ”Ž Validating Paths against API definitions:") - let validated = try pipeline.validate(parsed: parsed) - Console.print(result: "Checked \(validated.graphQLPaths.count) fields") + do { + Console.print(title: "๐Ÿ”Ž Validating Paths against API definitions:") + let validated = try pipeline.validate(parsed: parsed) + Console.print(result: "Checked \(validated.graphQLPaths.count) fields") - Console.print(title: "๐Ÿงฐ Resolving Fragments and Queries:") - let resolved = try pipeline.resolve(validated: validated) + Console.print(title: "๐Ÿงฐ Resolving Fragments and Queries:") + let resolved = try pipeline.resolve(validated: validated) - Console.print(result: "Resolved \(resolved.allQueries.count) Queries:") - resolved.allQueries.forEach { query in - Console.print(result: "\(inverse: query.name)", indentation: 2) - } + Console.print(result: "Resolved \(resolved.allQueries.count) Queries:") + resolved.allQueries.forEach { query in + Console.print(result: "\(inverse: query.name)", indentation: 2) + } - Console.print(result: "Resolved \(resolved.allFragments.count) Fragments:") - resolved.allFragments.forEach { fragment in - Console.print(result: "\(inverse: fragment.name)", indentation: 2) - } + Console.print(result: "Resolved \(resolved.allFragments.count) Fragments:") + resolved.allFragments.forEach { fragment in + Console.print(result: "\(inverse: fragment.name)", indentation: 2) + } - Console.print(title: "๐Ÿงน Cleaning Queries and Fragments:") - let cleaned = try pipeline.clean(resolved: resolved) + Console.print(title: "๐Ÿงน Cleaning Queries and Fragments:") + let cleaned = try pipeline.clean(resolved: resolved) - Console.print(title: "โœ๏ธ Generating Swift Code:") - - Console.print(title: "๐ŸŽจ Writing GraphQL Code", indentation: 1) - let assembled = try pipeline.assemble(cleaned: cleaned) - - Console.print(title: "๐Ÿš€ Delegating some stuff to Apollo codegen", indentation: 1) - let prepared = try pipeline.prepare(assembled: assembled, using: apollo) - - Console.print(title: "๐ŸŽ Bundling it all together", indentation: 1) - - let autoGeneratedFile = try pipeline.generate(prepared: prepared, useFormatting: !skipFormatting) - - Console.print(result: "Generated \(autoGeneratedFile.components(separatedBy: "\n").count) lines of code") - Console.print(result: "You're welcome ๐Ÿ™ƒ", indentation: 2) + Console.print(title: "โœ๏ธ Generating Swift Code:") + + Console.print(title: "๐ŸŽจ Writing GraphQL Code", indentation: 1) + let assembled = try pipeline.assemble(cleaned: cleaned) + + Console.print(title: "๐Ÿš€ Delegating some stuff to Apollo codegen", indentation: 1) + let prepared = try pipeline.prepare(assembled: assembled, using: apollo) - Console.print(title: "๐Ÿ’พ Saving Autogenerated Code") - try project.writeFile(name: "Graphaello.swift", content: autoGeneratedFile) + Console.print(title: "๐ŸŽ Bundling it all together", indentation: 1) - Console.print("") - Console.print(title: "โœ… Done") + let autoGeneratedFile = try pipeline.generate(prepared: prepared, useFormatting: !skipFormatting) + + Console.print(result: "Generated \(autoGeneratedFile.components(separatedBy: "\n").count) lines of code") + Console.print(result: "You're welcome ๐Ÿ™ƒ", indentation: 2) + + Console.print(title: "๐Ÿ’พ Saving Autogenerated Code") + try project.writeFile(name: "Graphaello.swift", content: autoGeneratedFile) + + Console.print("") + Console.print(title: "โœ… Done") + } catch { + cache?[.lastRunHash] = nil + throw error + } } } diff --git a/Sources/Graphaello/Processing/Model/Struct/Warning.swift b/Sources/Graphaello/Processing/Model/Struct/Warning.swift new file mode 100644 index 0000000..13b9b81 --- /dev/null +++ b/Sources/Graphaello/Processing/Model/Struct/Warning.swift @@ -0,0 +1,11 @@ + +import Foundation + +struct Warning: CustomStringConvertible { + let location: Location + let descriptionText: String + + var description: String { + return "\(location.locationDescription): warning: \(descriptionText)" + } +} diff --git a/Sources/Graphaello/Processing/Pipeline/BasicPipeline.swift b/Sources/Graphaello/Processing/Pipeline/BasicPipeline.swift index c16bde1..a026f83 100644 --- a/Sources/Graphaello/Processing/Pipeline/BasicPipeline.swift +++ b/Sources/Graphaello/Processing/Pipeline/BasicPipeline.swift @@ -10,6 +10,7 @@ struct BasicPipeline: Pipeline { let assembler: Assembler let preparator: Preparator let generator: Generator + var diagnoser: WarningDiagnoser? = nil func extract(from file: WithTargets) throws -> [Struct] { return try extractor.extract(from: file) @@ -44,4 +45,8 @@ struct BasicPipeline: Pipeline { func generate(prepared: Project.State, useFormatting: Bool) throws -> String { return try generator.generate(prepared: prepared, useFormatting: useFormatting) } + + func diagnose(parsed: Struct) throws -> [Warning] { + return try diagnoser?.diagnose(parsed: parsed) ?? [] + } } diff --git a/Sources/Graphaello/Processing/Pipeline/Component/1. Extraction/SourceCode/SourceCode+decode.swift b/Sources/Graphaello/Processing/Pipeline/Component/1. Extraction/SourceCode/SourceCode+decode.swift index 89d08f4..b1221df 100644 --- a/Sources/Graphaello/Processing/Pipeline/Component/1. Extraction/SourceCode/SourceCode+decode.swift +++ b/Sources/Graphaello/Processing/Pipeline/Component/1. Extraction/SourceCode/SourceCode+decode.swift @@ -75,6 +75,10 @@ extension SourceCode { return SwiftDeclarationAttributeKind(rawValue: try decode(key: "key.attribute")) ?? ._custom } + func usr() throws -> String { + return try decode(key: .usr) + } + } extension SourceCode { diff --git a/Sources/Graphaello/Processing/Pipeline/Diagnostics/Array+WarningDiagnoser.swift b/Sources/Graphaello/Processing/Pipeline/Diagnostics/Array+WarningDiagnoser.swift new file mode 100644 index 0000000..6a3b5f2 --- /dev/null +++ b/Sources/Graphaello/Processing/Pipeline/Diagnostics/Array+WarningDiagnoser.swift @@ -0,0 +1,10 @@ + +import Foundation + +extension Array: WarningDiagnoser where Element: WarningDiagnoser { + + func diagnose(parsed: Struct) throws -> [Warning] { + return try flatMap { try $0.diagnose(parsed: parsed) } + } + +} diff --git a/Sources/Graphaello/Processing/Pipeline/Diagnostics/UnusedWarningDiagnoser.swift b/Sources/Graphaello/Processing/Pipeline/Diagnostics/UnusedWarningDiagnoser.swift new file mode 100644 index 0000000..b5e7738 --- /dev/null +++ b/Sources/Graphaello/Processing/Pipeline/Diagnostics/UnusedWarningDiagnoser.swift @@ -0,0 +1,48 @@ + +import Foundation +import SwiftSyntax +import SourceKittenFramework + +private let swiftUIViewProtocols: Set = ["View"] + +struct UnusedWarningDiagnoser: WarningDiagnoser { + func diagnose(parsed: Struct) throws -> [Warning] { + guard !swiftUIViewProtocols.intersection(parsed.inheritedTypes).isEmpty else { + return [] + } + + return try parsed.properties.flatMap { try diagnose(property: $0, from: parsed) } + } + + private func diagnose(property: Property, from parsed: Struct) throws -> [Warning] { + guard property.graphqlPath != nil, + property.name != "id" else { return [] } + + let verifier = UsageVerifier(property: property) + let syntax = try parsed.code.syntaxTree() + + verifier.walk(syntax) + + guard !verifier.isUsed else { return [] } + return [Warning(location: property.code.location, + descriptionText: "Unused Property `\(property.name)` belongs to a View and is fetching data from GraphQL. This can be wasteful. Consider using it or removing the property.")] + } +} + +class UsageVerifier: SyntaxVisitor { + let property: Property + + private(set) var shouldUseSelf = false + private(set) var isUsed = false + + init(property: Property) { + self.property = property + super.init() + } + + override func visitPost(_ node: IdentifierExprSyntax) { + if node.identifier.text == property.name { + isUsed = true + } + } +} diff --git a/Sources/Graphaello/Processing/Pipeline/Diagnostics/WarningDiagnoser.swift b/Sources/Graphaello/Processing/Pipeline/Diagnostics/WarningDiagnoser.swift new file mode 100644 index 0000000..6c60884 --- /dev/null +++ b/Sources/Graphaello/Processing/Pipeline/Diagnostics/WarningDiagnoser.swift @@ -0,0 +1,6 @@ + +import Foundation + +protocol WarningDiagnoser { + func diagnose(parsed: Struct) throws -> [Warning] +} diff --git a/Sources/Graphaello/Processing/Pipeline/Pipeline.swift b/Sources/Graphaello/Processing/Pipeline/Pipeline.swift index fdfd8f3..b58c25e 100644 --- a/Sources/Graphaello/Processing/Pipeline/Pipeline.swift +++ b/Sources/Graphaello/Processing/Pipeline/Pipeline.swift @@ -14,6 +14,8 @@ protocol Pipeline { using apollo: ApolloReference) throws -> Project.State func generate(prepared: Project.State, useFormatting: Bool) throws -> String + + func diagnose(parsed: Struct) throws -> [Warning] } extension Pipeline { diff --git a/Sources/Graphaello/Processing/Pipeline/PipelineFactory.swift b/Sources/Graphaello/Processing/Pipeline/PipelineFactory.swift index baca3d2..52a4e1e 100644 --- a/Sources/Graphaello/Processing/Pipeline/PipelineFactory.swift +++ b/Sources/Graphaello/Processing/Pipeline/PipelineFactory.swift @@ -11,7 +11,8 @@ enum PipelineFactory { cleaner: create(), assembler: create(), preparator: create(), - generator: create()) + generator: create(), + diagnoser: UnusedWarningDiagnoser()) } private static func create() -> Extractor { diff --git a/Sources/Graphaello/Processing/Project/Project+State+Pipeline.swift b/Sources/Graphaello/Processing/Project/Project+State+Pipeline.swift index f3dfd53..114c3de 100644 --- a/Sources/Graphaello/Processing/Project/Project+State+Pipeline.swift +++ b/Sources/Graphaello/Processing/Project/Project+State+Pipeline.swift @@ -32,5 +32,9 @@ extension Pipeline { func clean(resolved: Project.State) throws -> Project.State { return resolved.with(structs: try clean(resolved: resolved.structs)) } + + func diagnose(parsed: Project.State) throws -> [Warning] { + return try parsed.structs.flatMap { try diagnose(parsed: $0) } + } }