Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,9 @@ Periphery can analyze projects using other build systems, though it cannot drive
"plists": [
"path/to/file.plist"
],
"asset_catalogs": [
"path/to/Assets.xcassets"
],
"xibs": [
"path/to/file.xib",
"path/to/file.storyboard"
Expand Down
5 changes: 5 additions & 0 deletions Sources/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ swift_library(
srcs = [
"SyntaxAnalysis/CommentCommand.swift",
"SyntaxAnalysis/DeclarationSyntaxVisitor.swift",
"SyntaxAnalysis/ImageAssetReferenceSyntaxVisitor.swift",
"SyntaxAnalysis/ImportSyntaxVisitor.swift",
"SyntaxAnalysis/MultiplexingSyntaxVisitor.swift",
"SyntaxAnalysis/SourceLOCCounter.swift",
Expand Down Expand Up @@ -52,6 +53,7 @@ swift_library(
"SourceGraph/Elements/Declaration.swift",
"SourceGraph/Elements/DeclarationAttribute.swift",
"SourceGraph/Elements/ImportStatement.swift",
"SourceGraph/Elements/ImageAssetReference.swift",
"SourceGraph/Elements/Location.swift",
"SourceGraph/Elements/ProjectFileKind.swift",
"SourceGraph/Elements/Reference.swift",
Expand All @@ -74,6 +76,7 @@ swift_library(
"SourceGraph/Mutators/GenericClassAndStructConstructorReferenceBuilder.swift",
"SourceGraph/Mutators/InheritedImplicitInitializerReferenceBuilder.swift",
"SourceGraph/Mutators/InterfaceBuilderPropertyRetainer.swift",
"SourceGraph/Mutators/ImageAssetReferenceRetainer.swift",
"SourceGraph/Mutators/ObjCAccessibleRetainer.swift",
"SourceGraph/Mutators/PropertyWrapperRetainer.swift",
"SourceGraph/Mutators/ProtocolConformanceReferenceBuilder.swift",
Expand Down Expand Up @@ -195,6 +198,8 @@ swift_library(
swift_library(
name = "Indexer",
srcs = [
"Indexer/AssetCatalogIndexer.swift",
"Indexer/AssetCatalogParser.swift",
"Indexer/IndexPipeline.swift",
"Indexer/IndexPlan.swift",
"Indexer/Indexer.swift",
Expand Down
36 changes: 36 additions & 0 deletions Sources/Indexer/AssetCatalogIndexer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Configuration
import Logger
import Shared
import SourceGraph
import SystemPackage

final class AssetCatalogIndexer: Indexer {
private let assetCatalogs: Set<FilePath>
private let graph: SourceGraphMutex
private let logger: ContextualLogger

required init(assetCatalogs: Set<FilePath>, graph: SourceGraphMutex, logger: ContextualLogger, configuration: Configuration) {
self.assetCatalogs = assetCatalogs
self.graph = graph
self.logger = logger.contextualized(with: "asset-catalog")
super.init(configuration: configuration)
}

func perform() throws {
let (includedFiles, excludedFiles) = filterIndexExcluded(from: assetCatalogs)
excludedFiles.forEach { self.logger.debug("Excluding \($0.string)") }

try JobPool(jobs: Array(includedFiles)).forEach { [weak self] assetCatalog in
guard let self else { return }

let elapsed = Benchmark.measure {
let imageAssets = AssetCatalogParser(path: assetCatalog).parse()
self.graph.withLock { graph in
imageAssets.forEach { graph.addImageAsset($0) }
}
}

logger.debug("\(assetCatalog.string) (\(elapsed)s)")
}
}
}
44 changes: 44 additions & 0 deletions Sources/Indexer/AssetCatalogParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Foundation
import SourceGraph
import SystemPackage

final class AssetCatalogParser {
private let path: FilePath

required init(path: FilePath) {
self.path = path
}

func parse() -> Set<Declaration> {
guard let enumerator = FileManager.default.enumerator(
at: path.url,
includingPropertiesForKeys: [.isDirectoryKey],
options: [.skipsHiddenFiles]
) else { return [] }

var declarations = Set<Declaration>()

for case let url as URL in enumerator {
guard url.pathExtension == "imageset" else { continue }

let imageSetPath = FilePath(url.path)
let name = url.deletingPathExtension().lastPathComponent
let contentsPath = imageSetPath.appending("Contents.json")
let locationPath = FileManager.default.fileExists(atPath: contentsPath.lexicallyNormalized().string) ? contentsPath : imageSetPath
let locationFile = SourceFile(path: locationPath, modules: [])
let location = Location(file: locationFile, line: 1, column: 1)
let usr = "image-asset-\(imageSetPath.lexicallyNormalized().string)"

declarations.insert(
Declaration(
name: name,
kind: .imageAsset,
usrs: [usr],
location: location
)
)
}

return declarations
}
}
9 changes: 9 additions & 0 deletions Sources/Indexer/IndexPipeline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ public struct IndexPipeline {
swiftVersion: swiftVersion
).perform()

if !plan.assetCatalogPaths.isEmpty {
try AssetCatalogIndexer(
assetCatalogs: plan.assetCatalogPaths,
graph: graph,
logger: logger,
configuration: configuration
).perform()
}

if !plan.plistPaths.isEmpty {
try InfoPlistIndexer(
infoPlistFiles: plan.plistPaths,
Expand Down
3 changes: 3 additions & 0 deletions Sources/Indexer/IndexPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@ import SystemPackage

public struct IndexPlan {
public let sourceFiles: [SourceFile: [IndexUnit]]
public let assetCatalogPaths: Set<FilePath>
public let plistPaths: Set<FilePath>
public let xibPaths: Set<FilePath>
public let xcDataModelPaths: Set<FilePath>
public let xcMappingModelPaths: Set<FilePath>

public init(
sourceFiles: [SourceFile: [IndexUnit]],
assetCatalogPaths: Set<FilePath> = [],
plistPaths: Set<FilePath> = [],
xibPaths: Set<FilePath> = [],
xcDataModelPaths: Set<FilePath> = [],
xcMappingModelPaths: Set<FilePath> = []
) {
self.sourceFiles = sourceFiles
self.assetCatalogPaths = assetCatalogPaths
self.plistPaths = plistPaths
self.xibPaths = xibPaths
self.xcDataModelPaths = xcDataModelPaths
Expand Down
5 changes: 5 additions & 0 deletions Sources/Indexer/SwiftIndexer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ final class SwiftIndexer: Indexer {
let multiplexingSyntaxVisitor = try MultiplexingSyntaxVisitor(file: sourceFile, swiftVersion: swiftVersion)
let declarationSyntaxVisitor = multiplexingSyntaxVisitor.add(DeclarationSyntaxVisitor.self)
let importSyntaxVisitor = multiplexingSyntaxVisitor.add(ImportSyntaxVisitor.self)
let imageAssetReferenceSyntaxVisitor = multiplexingSyntaxVisitor.add(ImageAssetReferenceSyntaxVisitor.self)

multiplexingSyntaxVisitor.visit()

Expand All @@ -279,6 +280,10 @@ final class SwiftIndexer: Indexer {
}
}

graph.withLock { graph in
imageAssetReferenceSyntaxVisitor.references.forEach { graph.add($0) }
}

associateLatentReferences()
associateDanglingReferences()
visitDeclarations(using: declarationSyntaxVisitor)
Expand Down
5 changes: 3 additions & 2 deletions Sources/Indexer/XibIndexer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ final class XibIndexer: Indexer {

let elapsed = try Benchmark.measure {
do {
let refs = try XibParser(path: xibPath)
let result = try XibParser(path: xibPath)
.parse()
self.graph.withLock { graph in
refs.forEach { graph.add($0) }
result.assetReferences.forEach { graph.add($0) }
result.imageAssetReferences.forEach { graph.add($0) }
}
} catch {
throw XibError.failedToParse(path: xibPath, underlyingError: error)
Expand Down
44 changes: 41 additions & 3 deletions Sources/Indexer/XibParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,15 @@ final class XibParser {
self.path = path
}

func parse() throws -> [AssetReference] {
guard let data = FileManager.default.contents(atPath: path.string) else { return [] }
struct Result {
let assetReferences: [AssetReference]
let imageAssetReferences: Set<ImageAssetReference>
}

func parse() throws -> Result {
guard let data = FileManager.default.contents(atPath: path.string) else {
return Result(assetReferences: [], imageAssetReferences: [])
}

let structure = try AEXMLDocument(xml: data)

Expand All @@ -22,8 +29,10 @@ final class XibParser {
// Collect all references with their outlets, actions, and runtime attributes
var referencesByClass: [String: (outlets: Set<String>, actions: Set<String>, runtimeAttributes: Set<String>)] = [:]
collectReferences(from: structure.root, idToCustomClass: idToCustomClass, into: &referencesByClass)
var imageReferences: Set<ImageAssetReference> = []
collectImageReferences(from: structure.root, into: &imageReferences)

return referencesByClass.map { className, members in
let assetReferences = referencesByClass.map { className, members in
AssetReference(
absoluteName: className,
source: .interfaceBuilder,
Expand All @@ -32,6 +41,8 @@ final class XibParser {
runtimeAttributes: Array(members.runtimeAttributes)
)
}

return Result(assetReferences: assetReferences, imageAssetReferences: imageReferences)
}

// MARK: - Private
Expand Down Expand Up @@ -180,4 +191,31 @@ final class XibParser {

return path
}

private func collectImageReferences(from element: AEXMLElement, into imageReferences: inout Set<ImageAssetReference>) {
for (key, value) in element.attributes {
let lowercasedKey = key.lowercased()

if lowercasedKey.contains("image"), lowercasedKey != "systemimage" {
imageReferences.insert(imageReference(named: value))
}
}

if element.name == "image", let name = element.attributes["name"] {
imageReferences.insert(imageReference(named: name))
}

for child in element.children {
collectImageReferences(from: child, into: &imageReferences)
}
}

private func imageReference(named name: String) -> ImageAssetReference {
let file = SourceFile(path: path, modules: [])
return ImageAssetReference(
name: name,
location: Location(file: file, line: 1, column: 1),
source: .interfaceBuilder
)
}
}
1 change: 1 addition & 0 deletions Sources/PeripheryKit/ScanResultBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public enum ScanResultBuilder {
let removableDeclarations = graph.unusedDeclarations
.subtracting(assignOnlyProperties)
.union(graph.unusedModuleImports)
.union(graph.unusedImageAssets)
let redundantProtocols = graph.redundantProtocols.filter { !removableDeclarations.contains($0.0) }
let redundantPublicAccessibility = graph.redundantPublicAccessibility.filter { !removableDeclarations.contains($0.0) }

Expand Down
7 changes: 7 additions & 0 deletions Sources/ProjectDrivers/GenericProjectDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import SystemPackage
public final class GenericProjectDriver {
struct GenericConfig: Decodable {
let indexstores: Set<String>
let assetCatalogs: Set<String>?
let plists: Set<String>
let xibs: Set<String>
let xcdatamodels: Set<String>
Expand All @@ -17,6 +18,7 @@ public final class GenericProjectDriver {
}

private let indexstorePaths: Set<FilePath>
private let assetCatalogPaths: Set<FilePath>
private let plistPaths: Set<FilePath>
private let xibPaths: Set<FilePath>
private let xcDataModelsPaths: Set<FilePath>
Expand All @@ -33,6 +35,7 @@ public final class GenericProjectDriver {
decoder.keyDecodingStrategy = .convertFromSnakeCase
let data = try Data(contentsOf: genericProjectConfig.url)
let config = try decoder.decode(GenericConfig.self, from: data)
let assetCatalogPaths = (config.assetCatalogs ?? []).mapSet { FilePath.makeAbsolute($0) }
let plistPaths = config.plists.mapSet { FilePath.makeAbsolute($0) }
let xibPaths = config.xibs.mapSet { FilePath.makeAbsolute($0) }
let xcDataModelPaths = config.xcdatamodels.mapSet { FilePath.makeAbsolute($0) }
Expand All @@ -41,6 +44,7 @@ public final class GenericProjectDriver {

self.init(
indexstorePaths: indexstorePaths,
assetCatalogPaths: assetCatalogPaths,
plistPaths: plistPaths,
xibPaths: xibPaths,
xcDataModelsPaths: xcDataModelPaths,
Expand All @@ -52,6 +56,7 @@ public final class GenericProjectDriver {

private init(
indexstorePaths: Set<FilePath>,
assetCatalogPaths: Set<FilePath>,
plistPaths: Set<FilePath>,
xibPaths: Set<FilePath>,
xcDataModelsPaths: Set<FilePath>,
Expand All @@ -60,6 +65,7 @@ public final class GenericProjectDriver {
configuration: Configuration
) {
self.indexstorePaths = indexstorePaths
self.assetCatalogPaths = assetCatalogPaths
self.plistPaths = plistPaths
self.xibPaths = xibPaths
self.xcDataModelsPaths = xcDataModelsPaths
Expand All @@ -82,6 +88,7 @@ extension GenericProjectDriver: ProjectDriver {

return IndexPlan(
sourceFiles: sourceFiles,
assetCatalogPaths: assetCatalogPaths,
plistPaths: plistPaths,
xibPaths: xibPaths,
xcDataModelPaths: xcDataModelsPaths,
Expand Down
45 changes: 45 additions & 0 deletions Sources/ProjectDrivers/SPMProjectDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,12 @@ extension SPMProjectDriver: ProjectDriver {
configuration: configuration
)
let sourceFiles = try collector.collect()
let assetCatalogPaths = assetCatalogs(from: description)
let xibPaths = interfaceBuilderFiles(from: description)

return IndexPlan(
sourceFiles: sourceFiles,
assetCatalogPaths: assetCatalogPaths,
xibPaths: xibPaths
)
}
Expand Down Expand Up @@ -100,4 +102,47 @@ extension SPMProjectDriver: ProjectDriver {

return xibFiles
}

private func assetCatalogs(from description: PackageDescription) -> Set<FilePath> {
var catalogs: Set<FilePath> = []

for target in description.targets {
let targetPath = pkg.path.appending(target.path)

guard let resources = target.resources else { continue }

for resource in resources {
let resourceFilePath = FilePath(resource.path)
let resourcePath: FilePath = resourceFilePath.isAbsolute
? resourceFilePath
: targetPath.appending(resource.path)

guard resourcePath.exists else { continue }

if resourcePath.extension?.lowercased() == "xcassets" {
catalogs.insert(resourcePath)
} else {
catalogs.formUnion(assetCatalogs(in: resourcePath))
}
}
}

return catalogs
}

private func assetCatalogs(in path: FilePath) -> Set<FilePath> {
guard let enumerator = FileManager.default.enumerator(
at: path.url,
includingPropertiesForKeys: [.isDirectoryKey],
options: [.skipsHiddenFiles]
) else { return [] }

var paths: Set<FilePath> = []

for case let url as URL in enumerator where url.pathExtension == "xcassets" {
paths.insert(FilePath(url.path))
}

return paths
}
}
Loading