Skip to content

Commit af80dd3

Browse files
authored
Merge pull request #97 from onevcat/codex/pr93-followup-generated-asset-symbols
Clarify and validate generated Objective-C asset symbol support
2 parents 81da1e6 + 981ffd6 commit af80dd3

12 files changed

Lines changed: 173 additions & 10 deletions

File tree

Sources/FengNiaoKit/Extensions.swift

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,23 @@ extension String {
5959

6060
/// Convert resource name (snake/kebab case) to generated Swift asset symbol such as `.icChatWhite`.
6161
var generatedAssetSymbolKey: String {
62-
if isEmpty { return "." }
63-
var ret = "."
64-
var shouldUpperNext = false
62+
return convertToCamelCase(prefix: ".", uppercaseFirst: false)
63+
}
64+
65+
/// Convert resource name (snake/kebab case) to generated Objective-C asset symbol such as `ACImageNameIcFlag`.
66+
/// Example: "ic_flag" -> "ACImageNameIcFlag"
67+
var objcGeneratedAssetSymbolKey: String {
68+
return convertToCamelCase(prefix: "ACImageName", uppercaseFirst: true)
69+
}
70+
71+
/// Convert resource name (snake/kebab case) to camel case with optional prefix.
72+
/// - Parameters:
73+
/// - prefix: The prefix to prepend to the result
74+
/// - uppercaseFirst: Whether the first character after prefix should be uppercase
75+
private func convertToCamelCase(prefix: String, uppercaseFirst: Bool) -> String {
76+
if isEmpty { return prefix }
77+
var ret = prefix
78+
var shouldUpperNext = uppercaseFirst
6579
for character in self {
6680
switch character {
6781
case "-", "_", " ":

Sources/FengNiaoKit/FengNiao.swift

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,21 @@ public struct FengNiao {
125125
let allResources = allResourceFiles()
126126
let usedNames = allUsedStringNames()
127127
let memberAccessUsedNames = allUsedMemberAccessNames()
128+
129+
// Generated asset symbols are an additional conservative usage signal.
130+
// Swift uses `.icFlag`, while Objective-C image symbols use `ACImageNameIcFlag`.
128131
let resourcesUsedByGeneratedSymbols = Set(
129-
allResources.keys.filter { memberAccessUsedNames.contains($0.generatedAssetSymbolKey) }
132+
allResources.keys.filter { resourceKey in
133+
let swiftSymbolKey = resourceKey.generatedAssetSymbolKey
134+
if memberAccessUsedNames.contains(swiftSymbolKey) {
135+
return true
136+
}
137+
let objcImageSymbolKey = resourceKey.objcGeneratedAssetSymbolKey
138+
if memberAccessUsedNames.contains(objcImageSymbolKey) {
139+
return true
140+
}
141+
return false
142+
}
130143
)
131144
let combinedUsedNames = usedNames.union(resourcesUsedByGeneratedSymbols)
132145

@@ -216,7 +229,8 @@ public struct FengNiao {
216229
}
217230

218231
func allUsedMemberAccessNames() -> Set<String> {
219-
guard searchInFileExtensions.contains("swift") else {
232+
let memberAccessExtensions: Set<String> = ["swift", "m", "mm", "h"]
233+
guard searchInFileExtensions.contains(where: { memberAccessExtensions.contains($0) }) else {
220234
return []
221235
}
222236
return usedMemberAccessNames(at: projectPath)
@@ -271,7 +285,9 @@ public struct FengNiao {
271285
return []
272286
}
273287

274-
let searchRule = SwiftMemberAccessSearchRule()
288+
let memberAccessExtensions: Set<String> = ["swift", "m", "mm", "h"]
289+
let swiftSearchRule = SwiftMemberAccessSearchRule()
290+
let objcSearchRule = ObjCMemberAccessSearchRule()
275291
var result = Set<String>()
276292
for subPath in subPaths {
277293
if subPath.lastComponent.hasPrefix(".") {
@@ -285,12 +301,23 @@ public struct FengNiao {
285301
if subPath.isDirectory {
286302
result.formUnion(usedMemberAccessNames(at: subPath))
287303
} else {
288-
guard (subPath.extension ?? "") == "swift" else {
304+
let fileExt = subPath.extension ?? ""
305+
guard searchInFileExtensions.contains(fileExt), memberAccessExtensions.contains(fileExt) else {
289306
continue
290307
}
291308

292309
let content = (try? subPath.read()) ?? ""
293-
result.formUnion(searchRule.search(in: content))
310+
311+
switch fileExt {
312+
case "swift":
313+
result.formUnion(swiftSearchRule.search(in: content))
314+
case "m", "mm", "h":
315+
// Scan headers conservatively as well, since projects may wrap generated
316+
// asset symbols in macros or inline helpers that are later imported into Swift.
317+
result.formUnion(objcSearchRule.search(in: content))
318+
default:
319+
break
320+
}
294321
}
295322
}
296323

Sources/FengNiaoKit/FileSearchRule.swift

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ struct SwiftMemberAccessSearchRule: FileSearchRule {
8080
func search(in content: String) -> Set<String> {
8181
let nsstring = NSString(string: content)
8282
var result = Set<String>()
83-
let pattern = #"(?<![A-Za-z0-9_])(UIImage|UIColor|NSImage|NSColor|Image|Color)?\s*\.\s*([A-Za-z0-9_]+)"#
83+
let pattern = #"(?<![A-Za-z0-9_])(ImageResource|UIImage|UIColor|NSImage|NSColor|Image|Color)?\s*\.\s*([A-Za-z0-9_]+)"#
8484
let reg = try! NSRegularExpression(pattern: pattern, options: [])
8585
let matches = reg.matches(in: content, options: [], range: content.fullRange)
8686
for match in matches {
@@ -93,6 +93,35 @@ struct SwiftMemberAccessSearchRule: FileSearchRule {
9393
}
9494
}
9595

96+
/// Search for generated Objective-C image asset symbols such as `ACImageNameIcFlag`.
97+
/// Example: "ic_flag.png" -> "ACImageNameIcFlag"
98+
struct ObjCMemberAccessSearchRule: FileSearchRule {
99+
func search(in content: String) -> Set<String> {
100+
let nsstring = NSString(string: content)
101+
var result = Set<String>()
102+
103+
result.formUnion(matchSymbols(in: content, prefix: "ACImageName", nsstring: nsstring))
104+
105+
return result
106+
}
107+
108+
private func matchSymbols(in content: String, prefix: String, nsstring: NSString) -> Set<String> {
109+
var result = Set<String>()
110+
// Escape the prefix for use in regex pattern
111+
let escapedPrefix = NSRegularExpression.escapedPattern(for: prefix)
112+
let pattern = "\(escapedPrefix)([A-Z][a-zA-Z0-9]*)"
113+
let reg = try! NSRegularExpression(pattern: pattern, options: [])
114+
let matches = reg.matches(in: content, options: [], range: content.fullRange)
115+
for match in matches {
116+
let identifierRange = match.range(at: 1)
117+
guard identifierRange.location != NSNotFound else { continue }
118+
let identifier = nsstring.substring(with: identifierRange)
119+
result.insert("\(prefix)\(identifier)")
120+
}
121+
return result
122+
}
123+
}
124+
96125
struct XibImageSearchRule: RegPatternSearchRule {
97126
let extensions = [String]()
98127
let patterns = ["image name=\"(.*?)\"", "image=\"(.*?)\"", "value=\"(.*?)\""]

Tests/FengNiaoKitTests/GeneratedAssetSymbolTests.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,34 @@ struct GeneratedAssetSymbolTests {
2020
let expected: Set<String> = ["ic_unused.png"]
2121
#expect(fileNames == expected)
2222
}
23+
24+
@Test("treats generated Objective-C asset symbols in implementation files as usage")
25+
func treatsGeneratedObjectiveCAssetSymbolsAsUsage() throws {
26+
let project = fixtures + "GeneratedAssetSymbolObjC"
27+
let fengniao = FengNiao(
28+
projectPath: project.string,
29+
excludedPaths: [],
30+
resourceExtensions: ["png"],
31+
searchInFileExtensions: ["m"]
32+
)
33+
let result = try fengniao.unusedFiles()
34+
let fileNames = Set(result.map { $0.fileName })
35+
let expected: Set<String> = ["ic_unused.png"]
36+
#expect(fileNames == expected)
37+
}
38+
39+
@Test("treats generated Objective-C asset symbols in headers as usage conservatively")
40+
func treatsGeneratedObjectiveCAssetSymbolsInHeadersAsUsageConservatively() throws {
41+
let project = fixtures + "GeneratedAssetSymbolHeader"
42+
let fengniao = FengNiao(
43+
projectPath: project.string,
44+
excludedPaths: [],
45+
resourceExtensions: ["png"],
46+
searchInFileExtensions: ["h"]
47+
)
48+
let result = try fengniao.unusedFiles()
49+
let fileNames = Set(result.map { $0.fileName })
50+
let expected: Set<String> = ["ic_unused.png"]
51+
#expect(fileNames == expected)
52+
}
2353
}

Tests/FengNiaoKitTests/SearchRuleTests.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,13 @@ struct SearchRuleTests {
8585
let searcher = SwiftMemberAccessSearchRule()
8686
let content = """
8787
let flag = UIImage.icFlag
88+
let flag1 = ImageResource.icFlag1
8889
let highlighted: UIImage = .icFlagHighlighted
8990
let legacy = NSImage .icFlagSecondary
9091
let accent = Color .customAccent
9192
"""
9293
let result = searcher.search(in: content)
93-
let expected: Set<String> = [".icFlag", ".icFlagHighlighted", ".icFlagSecondary", ".customAccent"]
94+
let expected: Set<String> = [".icFlag", ".icFlag1", ".icFlagHighlighted", ".icFlagSecondary", ".customAccent"]
9495
#expect(result == expected)
9596
}
9697

@@ -134,4 +135,33 @@ struct SearchRuleTests {
134135
let result = searcher.search(in: content)
135136
#expect(result.isEmpty)
136137
}
138+
139+
@Test("Objective-C member access rule applies to generated symbols")
140+
func objcMemberAccessRuleAppliesToGeneratedSymbols() {
141+
let searcher = ObjCMemberAccessSearchRule()
142+
let content = """
143+
UIImage *flag = [UIImage imageNamed:ACImageNameIcFlag];
144+
UIImage *highlighted = [UIImage imageNamed:ACImageNameIcFlagHighlighted];
145+
NSImage *legacy = [NSImage imageNamed:ACImageNameIcFlagSecondary];
146+
NSString *name = ACImageNameIcFlag;
147+
"""
148+
let result = searcher.search(in: content)
149+
let expected: Set<String> = [
150+
"ACImageNameIcFlag",
151+
"ACImageNameIcFlagHighlighted",
152+
"ACImageNameIcFlagSecondary",
153+
]
154+
#expect(result == expected)
155+
}
156+
157+
@Test("Objective-C member access rule ignores regular constants")
158+
func objcMemberAccessRuleIgnoresRegularConstants() {
159+
let searcher = ObjCMemberAccessSearchRule()
160+
let content = """
161+
NSString *name = kImageName;
162+
NSString *other = SomeOtherConstant;
163+
"""
164+
let result = searcher.search(in: content)
165+
#expect(result.isEmpty)
166+
}
137167
}

Tests/FengNiaoKitTests/StringExtensionsTests.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,21 @@ struct StringExtensionsTests {
4141
]
4242
#expect(images.map { $0.generatedAssetSymbolKey } == expected)
4343
}
44+
45+
@Test("objcGeneratedAssetSymbolKey converts to Objective-C format")
46+
func objcGeneratedAssetSymbolKeyConvertsToObjectiveCFormat() {
47+
let images = [
48+
"ic_chat_white_24px",
49+
"ic-chat_white_24 px",
50+
"iC-ChAt_whIte_24 pX",
51+
"ICCHATWHITE",
52+
]
53+
let expected = [
54+
"ACImageNameIcChatWhite24Px",
55+
"ACImageNameIcChatWhite24Px",
56+
"ACImageNameICChAtWhIte24PX",
57+
"ACImageNameICCHATWHITE",
58+
]
59+
#expect(images.map { $0.objcGeneratedAssetSymbolKey } == expected)
60+
}
4461
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#import <UIKit/UIKit.h>
2+
3+
#define FNFlagAssetName ACImageNameIcFlag
4+
5+
NS_INLINE UIImage *FNFlagImage(void) {
6+
return [UIImage imageNamed:ACImageNameIcFlag];
7+
}
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#import <UIKit/UIKit.h>
2+
3+
void configureFlagView(UIImageView *imageView) {
4+
imageView.image = [UIImage imageNamed:ACImageNameIcFlag];
5+
}

0 commit comments

Comments
 (0)