Created
February 8, 2025 02:46
-
-
Save Saik0s/a6f09e3f1cdeb6424ce343609a8fd581 to your computer and use it in GitHub Desktop.
sourcery template for generating a code index file from swift source files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
description: Code index maps all types, methods and properties across the project files. Mirrors folder structure to give instant high-level view of codebase architecture - perfect for grasping project scope and organization at a glance. | |
globs: **/*.{swift,md} | |
--- | |
Format: | |
./Directory/ | |
struct TypeName: Protocols | |
- property: Type | |
- method(param: Type) -> ReturnType | |
Excludes: | |
- Private/internal implementation details | |
- Boilerplate code | |
- Types marked with // sourcery: skipIndex | |
<% | |
/* --------------------------------------------------------------------- | |
1. Directory Tree Data Structure | |
--------------------------------------------------------------------- */ | |
class DirectoryNode { | |
var name: String | |
var children: [String: DirectoryNode] = [:] | |
var types: [Type] = [] | |
init(name: String) { | |
self.name = name | |
} | |
func insertType(_ type: Type, pathComponents: [String]) { | |
guard !pathComponents.isEmpty else { | |
self.types.append(type) | |
return | |
} | |
let head = pathComponents[0] | |
let tail = Array(pathComponents.dropFirst()) | |
if children[head] == nil { | |
children[head] = DirectoryNode(name: head) | |
} | |
children[head]!.insertType(type, pathComponents: tail) | |
} | |
func sortRecursively() { | |
types.sort { $0.name < $1.name } | |
for child in children.values { | |
child.sortRecursively() | |
} | |
} | |
} | |
/* --------------------------------------------------------------------- | |
2. Filtering & Building the Directory | |
--------------------------------------------------------------------- */ | |
// Skip any type that has `// sourcery: skipIndex` | |
func shouldIncludeType(_ t: Type) -> Bool { | |
if t.annotations["skipIndex"] != nil { | |
return false | |
} | |
return true | |
} | |
func buildDirectoryTree() -> DirectoryNode { | |
let root = DirectoryNode(name: "ROOT") | |
for type in types.all { | |
guard shouldIncludeType(type) else { continue } | |
// 'directory' is optional string | |
let dir = type.directory ?? "" | |
let dirPath = dir.trimmingCharacters(in: CharacterSet(charactersIn: "/")) | |
let components = dirPath.split(separator: "/").map(String.init) | |
root.insertType(type, pathComponents: components) | |
} | |
root.sortRecursively() | |
return root | |
} | |
/* --------------------------------------------------------------------- | |
3. Access Level & Boilerplate Checks | |
--------------------------------------------------------------------- */ | |
// method.accessLevel is a String (or String?). | |
func methodAccessIsPrivate(_ m: SourceryRuntime.Method) -> Bool { | |
let level = m.accessLevel | |
// Compare to string "private"/"fileprivate" | |
return (level == "private" || level == "fileprivate") | |
} | |
// Variables generally have `readAccess` & `writeAccess` as Strings. | |
func variableAccessIsPrivate(_ v: Variable) -> Bool { | |
// If either is private/fileprivate, skip | |
let read = v.readAccess | |
let write = v.writeAccess | |
if read == "private" || read == "fileprivate" { return true } | |
if write == "private" || write == "fileprivate" { return true } | |
return false | |
} | |
// Remove standard Swift boilerplate | |
func isBoilerplateMethod(_ method: SourceryRuntime.Method) -> Bool { | |
if method.callName == "init" { return true } | |
if method.callName == "==" { return true } | |
if method.callName == "hash" { return true } | |
return false | |
} | |
func isSwiftUIBody(_ varName: String, _ type: Type) -> Bool { | |
let inheritsView = type.inheritedTypes.contains("View") | |
let implementsView = type.implements.values.contains { $0.name == "View" } | |
return (inheritsView || implementsView) && (varName == "body") | |
} | |
/* --------------------------------------------------------------------- | |
4. Rendering | |
--------------------------------------------------------------------- */ | |
func renderDirectoryNode(_ node: DirectoryNode, level: Int = 0) -> String { | |
let indent = String(repeating: " ", count: max(0, level - 1)) | |
var output = "" | |
// Handle directory name display with merging single children | |
if node.name != "ROOT" { | |
var currentNode = node | |
var pathComponents = [node.name] | |
// Keep merging while there's exactly one child and no types | |
while currentNode.children.count == 1 && currentNode.types.isEmpty { | |
let childNode = currentNode.children.values.first! | |
pathComponents.append(childNode.name) | |
currentNode = childNode | |
} | |
if level > 0 { | |
output += "\(indent)./\(pathComponents.joined(separator: "/"))/\n" | |
} | |
// Render types for the last node in chain | |
for t in currentNode.types { | |
output += renderType(t, level: level + 1) | |
} | |
// Render remaining children | |
let sortedChildNames = currentNode.children.keys.sorted() | |
for childName in sortedChildNames { | |
if let childNode = currentNode.children[childName] { | |
output += renderDirectoryNode(childNode, level: level + 1) | |
} | |
} | |
return output | |
} | |
// Root node handling remains the same | |
for t in node.types { | |
output += renderType(t, level: level + 1) | |
} | |
let sortedChildNames = node.children.keys.sorted() | |
for childName in sortedChildNames { | |
if let childNode = node.children[childName] { | |
output += renderDirectoryNode(childNode, level: level) | |
} | |
} | |
return output.replacingOccurrences(of: "UnknownTypeSoAddTypeAttributionToVariable", with: "") | |
} | |
func renderType(_ t: Type, level: Int) -> String { | |
var result = "" | |
let indent = String(repeating: " ", count: max(0, level - 1)) | |
// documentation is an array of strings | |
if !t.documentation.isEmpty { | |
for docLine in t.documentation { | |
result += "\(indent)/// \(docLine)\n" | |
} | |
} | |
result += "\(indent)\(t.kind) \(t.name)" | |
// Inherits & Implements | |
let filteredInherits = t.inheritedTypes.filter { !["Codable", "Equatable", "Identifiable", "Hashable", "Encodable", "Decodable"].contains($0) } | |
if !filteredInherits.isEmpty { | |
result += ": \(filteredInherits.joined(separator: ", "))" | |
} | |
let filteredImplements = t.implements.filter { !["Codable", "Equatable", "Identifiable", "Hashable", "Encodable", "Decodable"].contains($0.value.name) } | |
if !filteredImplements.isEmpty { | |
let addedColon = !filteredInherits.isEmpty | |
let implemented = filteredImplements.map { $0.value.name }.joined(separator: ", ") | |
result += (addedColon ? ", " : ": ") + implemented | |
} | |
result += "\n" | |
// Properties | |
let filteredVars = t.variables.filter { | |
!variableAccessIsPrivate($0) && !isSwiftUIBody($0.name, t) && !$0.isStatic | |
} | |
if !filteredVars.isEmpty { | |
for v in filteredVars { | |
result += "\(indent) - \(v.name): \(v.typeName.name)\n" | |
} | |
} | |
// Methods | |
let filteredMethods = t.methods.filter { | |
!methodAccessIsPrivate($0) && !isBoilerplateMethod($0) && !$0.isStatic | |
} | |
if !filteredMethods.isEmpty { | |
if !filteredVars.isEmpty { | |
result += "\n" | |
} | |
for m in filteredMethods { | |
result += "\(indent) - \(methodSignature(m))\n" | |
} | |
} | |
// Static Variables | |
let filteredStaticVars = t.staticVariables.filter { | |
!variableAccessIsPrivate($0) && !isSwiftUIBody($0.name, t) | |
} | |
if !filteredStaticVars.isEmpty { | |
for sv in filteredStaticVars { | |
result += "\(indent) + \(sv.name): \(sv.typeName.name)\n" | |
} | |
} | |
// Static Methods | |
let filteredStaticMethods = t.staticMethods.filter { | |
!methodAccessIsPrivate($0) && !isBoilerplateMethod($0) && !$0.isStatic | |
} | |
if !filteredStaticMethods.isEmpty { | |
if !filteredVars.isEmpty { | |
result += "\n" | |
} | |
for sm in filteredStaticMethods { | |
result += "\(indent) + \(methodSignature(sm))\n" | |
} | |
} | |
result += "\n" | |
return result | |
} | |
func methodSignature(_ method: SourceryRuntime.Method) -> String { | |
let throwMods = [ | |
method.`throws` ? "throws" : "", | |
method.`rethrows` ? "rethrows" : "" | |
].filter { !$0.isEmpty }.joined(separator: " ") | |
let returnName = method.returnTypeName.name | |
let returnSegment = returnName.isEmpty ? "" : " -> \(returnName)" | |
return "\(method.name)" + | |
(throwMods.isEmpty ? "" : " \(throwMods)") + | |
returnSegment | |
} | |
%> | |
<% | |
/* --------------------------------------------------------------------- | |
5. MAIN TEMPLATE ENTRY | |
--------------------------------------------------------------------- */ | |
let root = buildDirectoryTree() | |
-%> | |
<%= renderDirectoryNode(root) %> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment