Skip to content

Commit 80d0acb

Browse files
authored
Simplify Parser and Introduce Formatter (#238)
## Changes ### Major Simplify `Parser` to exclusively implement its intended functionality: parsing. Separate formatting-related logic into a new type, `Formatter`. Move the `beautify` method from `OutputRendering` to `Formatter`, delete the unnecessary extension on `String`, and update `JunitReporter` as necessary. Update `XCBeautifier` and the executable to wrap `Parser`, `Formatter`, and output-related logic. ### Minor - Make types `package`-accessible, as necessary. - Update unit tests to support new implementation. ## Testing ### Performance | | Old (Seconds) | New (Seconds) | Difference (%) | |:---:|:---:|:---:|:---:| | Run 1 | 4.84 | 4.88 | - | | Run 2 | 4.87 | 4.73 | - | | Run 3 | 4.82 | 4.79 | - | | Run 4 | 4.93 | 4.73 | - | | Run 5 | 4.87 | 4.75 | - | | Average | 4.866 | 4.776 | -1.87% |
1 parent 8c8be6a commit 80d0acb

13 files changed

+313
-502
lines changed

Sources/XcbeautifyLib/CaptureGroups.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import Foundation
22

3-
protocol CaptureGroup {
3+
package protocol CaptureGroup {
44
static var outputType: OutputType { get }
55
static var regex: XcbeautifyLib.Regex { get }
66
init?(groups: [String])
77
}
88

9+
package extension CaptureGroup {
10+
var outputType: OutputType { Self.outputType }
11+
}
12+
913
extension CaptureGroup {
1014
static var pattern: String { regex.pattern }
1115
var pattern: String { Self.regex.pattern }

Sources/XcbeautifyLib/Formatter.swift

+211
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import Foundation
2+
3+
package struct Formatter {
4+
private let colored: Bool
5+
private let renderer: any OutputRendering
6+
7+
package init(
8+
colored: Bool,
9+
renderer: Renderer,
10+
additionalLines: @escaping () -> (String?)
11+
) {
12+
self.colored = colored
13+
14+
switch renderer {
15+
case .terminal:
16+
self.renderer = TerminalRenderer(colored: colored, additionalLines: additionalLines)
17+
case .gitHubActions:
18+
self.renderer = GitHubActionsRenderer(colored: colored, additionalLines: additionalLines)
19+
case .teamcity:
20+
self.renderer = TeamCityRenderer(colored: colored, additionalLines: additionalLines)
21+
}
22+
}
23+
24+
package func format(captureGroup: any CaptureGroup) -> String? {
25+
switch captureGroup {
26+
case let group as AggregateTargetCaptureGroup:
27+
return renderer.formatTargetCommand(command: "Aggregate", group: group)
28+
case let group as AnalyzeCaptureGroup:
29+
return renderer.formatAnalyze(group: group)
30+
case let group as AnalyzeTargetCaptureGroup:
31+
return renderer.formatTargetCommand(command: "Analyze", group: group)
32+
case let group as BuildTargetCaptureGroup:
33+
return renderer.formatTargetCommand(command: "Build", group: group)
34+
case is CheckDependenciesCaptureGroup:
35+
return renderer.formatCheckDependencies()
36+
case let group as CheckDependenciesErrorsCaptureGroup:
37+
return renderer.formatError(group: group)
38+
case let group as ClangErrorCaptureGroup:
39+
return renderer.formatError(group: group)
40+
case let group as CleanRemoveCaptureGroup:
41+
return renderer.formatCleanRemove(group: group)
42+
case let group as CleanTargetCaptureGroup:
43+
return renderer.formatTargetCommand(command: "Clean", group: group)
44+
case let group as CodesignCaptureGroup:
45+
return renderer.formatCodeSign(group: group)
46+
case let group as CodesignFrameworkCaptureGroup:
47+
return renderer.formatCodeSignFramework(group: group)
48+
case let group as CompilationResultCaptureGroup:
49+
return renderer.formatCompilationResult(group: group)
50+
case let group as CompileCaptureGroup:
51+
return renderer.formatCompile(group: group)
52+
case let group as SwiftCompileCaptureGroup:
53+
return renderer.formatCompile(group: group)
54+
case let group as SwiftCompilingCaptureGroup:
55+
return renderer.formatSwiftCompiling(group: group)
56+
case let group as CompileCommandCaptureGroup:
57+
return renderer.formatCompileCommand(group: group)
58+
case let group as CompileErrorCaptureGroup:
59+
return renderer.formatCompileError(group: group)
60+
case let group as CompileStoryboardCaptureGroup:
61+
return renderer.formatCompile(group: group)
62+
case let group as CompileWarningCaptureGroup:
63+
return renderer.formatCompileWarning(group: group)
64+
case let group as CompileXibCaptureGroup:
65+
return renderer.formatCompile(group: group)
66+
case let group as CopyHeaderCaptureGroup:
67+
return renderer.formatCopy(group: group)
68+
case let group as CopyPlistCaptureGroup:
69+
return renderer.formatCopy(group: group)
70+
case let group as CopyStringsCaptureGroup:
71+
return renderer.formatCopy(group: group)
72+
case let group as CpresourceCaptureGroup:
73+
return renderer.formatCopy(group: group)
74+
case let group as CursorCaptureGroup:
75+
return renderer.formatCursor(group: group)
76+
case let group as DuplicateLocalizedStringKeyCaptureGroup:
77+
return renderer.formatDuplicateLocalizedStringKey(group: group)
78+
case let group as ExecutedWithoutSkippedCaptureGroup:
79+
return renderer.formatExecutedWithoutSkipped(group: group)
80+
case let group as ExecutedWithSkippedCaptureGroup:
81+
return renderer.formatExecutedWithSkipped(group: group)
82+
case let group as FailingTestCaptureGroup:
83+
return renderer.formatFailingTest(group: group)
84+
case let group as FatalErrorCaptureGroup:
85+
return renderer.formatError(group: group)
86+
case let group as FileMissingErrorCaptureGroup:
87+
return renderer.formatFileMissingError(group: group)
88+
case let group as GenerateCoverageDataCaptureGroup:
89+
return renderer.formatGenerateCoverageData(group: group)
90+
case let group as GeneratedCoverageReportCaptureGroup:
91+
return renderer.formatCoverageReport(group: group)
92+
case let group as GenerateDSYMCaptureGroup:
93+
return renderer.formatGenerateDsym(group: group)
94+
case let group as GenericWarningCaptureGroup:
95+
return renderer.formatWarning(group: group)
96+
case let group as LDErrorCaptureGroup:
97+
return renderer.formatError(group: group)
98+
case let group as LDWarningCaptureGroup:
99+
return renderer.formatLdWarning(group: group)
100+
case let group as LibtoolCaptureGroup:
101+
return renderer.formatLibtool(group: group)
102+
case let group as LinkerDuplicateSymbolsCaptureGroup:
103+
return renderer.formatLinkerDuplicateSymbolsError(group: group)
104+
case let group as LinkerDuplicateSymbolsLocationCaptureGroup:
105+
return renderer.formatLinkerDuplicateSymbolsLocation(group: group)
106+
case let group as LinkerUndefinedSymbolLocationCaptureGroup:
107+
return renderer.formatLinkerUndefinedSymbolLocation(group: group)
108+
case let group as LinkerUndefinedSymbolsCaptureGroup:
109+
return renderer.formatLinkerUndefinedSymbolsError(group: group)
110+
case let group as LinkingCaptureGroup:
111+
return renderer.formatLinking(group: group)
112+
case let group as ModuleIncludesErrorCaptureGroup:
113+
return renderer.formatError(group: group)
114+
case let group as NoCertificateCaptureGroup:
115+
return renderer.formatError(group: group)
116+
case let group as PackageCheckingOutCaptureGroup:
117+
return renderer.formatPackageCheckingOut(group: group)
118+
case let group as PackageFetchingCaptureGroup:
119+
return renderer.formatPackageFetching(group: group)
120+
case let group as PackageGraphResolvedItemCaptureGroup:
121+
return renderer.formatPackageItem(group: group)
122+
case is PackageGraphResolvingEndedCaptureGroup:
123+
return renderer.formatPackageEnd()
124+
case is PackageGraphResolvingStartCaptureGroup:
125+
return renderer.formatPackageStart()
126+
case let group as PackageUpdatingCaptureGroup:
127+
return renderer.formatPackageUpdating(group: group)
128+
case let group as ParallelTestCaseAppKitPassedCaptureGroup:
129+
return renderer.formatParallelTestCaseAppKitPassed(group: group)
130+
case let group as ParallelTestCaseFailedCaptureGroup:
131+
return renderer.formatParallelTestCaseFailed(group: group)
132+
case let group as ParallelTestCasePassedCaptureGroup:
133+
return renderer.formatParallelTestCasePassed(group: group)
134+
case let group as ParallelTestCaseSkippedCaptureGroup:
135+
return renderer.formatParallelTestCaseSkipped(group: group)
136+
case let group as ParallelTestingFailedCaptureGroup:
137+
return renderer.formatParallelTestingFailed(group: group)
138+
case let group as ParallelTestingPassedCaptureGroup:
139+
return renderer.formatParallelTestingPassed(group: group)
140+
case let group as ParallelTestingStartedCaptureGroup:
141+
return renderer.formatParallelTestingStarted(group: group)
142+
case let group as ParallelTestSuiteStartedCaptureGroup:
143+
return renderer.formatParallelTestSuiteStarted(group: group)
144+
case let group as PbxcpCaptureGroup:
145+
return renderer.formatCopy(group: group)
146+
case let group as PhaseScriptExecutionCaptureGroup:
147+
return renderer.formatPhaseScriptExecution(group: group)
148+
case let group as PhaseSuccessCaptureGroup:
149+
return renderer.formatPhaseSuccess(group: group)
150+
case let group as PodsErrorCaptureGroup:
151+
return renderer.formatError(group: group)
152+
case let group as PreprocessCaptureGroup:
153+
return renderer.formatPreprocess(group: group)
154+
case let group as ProcessInfoPlistCaptureGroup:
155+
return renderer.formatProcessInfoPlist(group: group)
156+
case let group as ProcessPchCaptureGroup:
157+
return renderer.formatProcessPch(group: group)
158+
case let group as ProcessPchCommandCaptureGroup:
159+
return renderer.formatProcessPchCommand(group: group)
160+
case let group as ProvisioningProfileRequiredCaptureGroup:
161+
return renderer.formatError(group: group)
162+
case let group as RestartingTestCaptureGroup:
163+
return renderer.formatRestartingTest(group: group)
164+
case let group as ShellCommandCaptureGroup:
165+
return renderer.formatShellCommand(group: group)
166+
case let group as SymbolReferencedFromCaptureGroup:
167+
return renderer.formatSymbolReferencedFrom(group: group)
168+
case let group as TestCaseMeasuredCaptureGroup:
169+
return renderer.formatTestCaseMeasured(group: group)
170+
case let group as TestCasePassedCaptureGroup:
171+
return renderer.formatTestCasePassed(group: group)
172+
case let group as TestCaseSkippedCaptureGroup:
173+
return renderer.formatTestCaseSkipped(group: group)
174+
case let group as TestCasePendingCaptureGroup:
175+
return renderer.formatTestCasePending(group: group)
176+
case let group as TestCaseStartedCaptureGroup:
177+
return renderer.formatTestCasesStarted(group: group)
178+
case let group as TestsRunCompletionCaptureGroup:
179+
return renderer.formatTestsRunCompletion(group: group)
180+
case let group as TestSuiteAllTestsFailedCaptureGroup:
181+
return renderer.formatTestSuiteAllTestsFailed(group: group)
182+
case let group as TestSuiteAllTestsPassedCaptureGroup:
183+
return renderer.formatTestSuiteAllTestsPassed(group: group)
184+
case let group as TestSuiteStartCaptureGroup:
185+
return renderer.formatTestSuiteStart(group: group)
186+
case let group as TestSuiteStartedCaptureGroup:
187+
return renderer.formatTestSuiteStarted(group: group)
188+
case let group as TIFFutilCaptureGroup:
189+
return renderer.formatTIFFUtil(group: group)
190+
case let group as TouchCaptureGroup:
191+
return renderer.formatTouch(group: group)
192+
case let group as UIFailingTestCaptureGroup:
193+
return renderer.formatUIFailingTest(group: group)
194+
case let group as UndefinedSymbolLocationCaptureGroup:
195+
return renderer.formatUndefinedSymbolLocation(group: group)
196+
case let group as WillNotBeCodeSignedCaptureGroup:
197+
return renderer.formatWillNotBeCodesignWarning(group: group)
198+
case let group as WriteAuxiliaryFileCaptureGroup:
199+
return renderer.formatWriteAuxiliaryFile(group: group)
200+
case let group as WriteFileCaptureGroup:
201+
return renderer.formatWriteFile(group: group)
202+
case let group as XcodebuildErrorCaptureGroup:
203+
return renderer.formatError(group: group)
204+
case let group as SwiftDriverJobDiscoveryEmittingModuleCaptureGroup:
205+
return renderer.formatSwiftDriverJobDiscoveryEmittingModule(group: group)
206+
default:
207+
assertionFailure()
208+
return nil
209+
}
210+
}
211+
}

Sources/XcbeautifyLib/JunitReporter.swift

+16-16
Original file line numberDiff line numberDiff line change
@@ -50,51 +50,51 @@ package final class JunitReporter {
5050
}
5151

5252
private func generateFailingTest(line: String) -> TestCase? {
53-
guard let _group: any CaptureGroup = line.captureGroup(with: FailingTestCaptureGroup.pattern) else { return nil }
54-
guard let group = _group as? FailingTestCaptureGroup else { return nil }
53+
let groups = FailingTestCaptureGroup.regex.captureGroups(for: line)
54+
guard let group = FailingTestCaptureGroup(groups: groups) else { return nil }
5555
return TestCase(classname: group.testSuite, name: group.testCase, time: nil, failure: .init(message: "\(group.file) - \(group.reason)"))
5656
}
5757

5858
private func generateRestartingTest(line: String) -> TestCase? {
59-
guard let _group: any CaptureGroup = line.captureGroup(with: RestartingTestCaptureGroup.pattern) else { return nil }
60-
guard let group = _group as? RestartingTestCaptureGroup else { return nil }
59+
let groups = RestartingTestCaptureGroup.regex.captureGroups(for: line)
60+
guard let group = RestartingTestCaptureGroup(groups: groups) else { return nil }
6161
return TestCase(classname: group.testSuite, name: group.testCase, time: nil, failure: .init(message: line))
6262
}
6363

6464
private func generateParallelFailingTest(line: String) -> TestCase? {
6565
// Parallel tests do not provide meaningful failure messages
66-
guard let _group: any CaptureGroup = line.captureGroup(with: ParallelTestCaseFailedCaptureGroup.pattern) else { return nil }
67-
guard let group = _group as? ParallelTestCaseFailedCaptureGroup else { return nil }
66+
let groups = ParallelTestCaseFailedCaptureGroup.regex.captureGroups(for: line)
67+
guard let group = ParallelTestCaseFailedCaptureGroup(groups: groups) else { return nil }
6868
return TestCase(classname: group.suite, name: group.testCase, time: nil, failure: .init(message: "Parallel test failed"))
6969
}
7070

7171
private func generatePassingTest(line: String) -> TestCase? {
72-
guard let _group: any CaptureGroup = line.captureGroup(with: TestCasePassedCaptureGroup.pattern) else { return nil }
73-
guard let group = _group as? TestCasePassedCaptureGroup else { return nil }
72+
let groups = TestCasePassedCaptureGroup.regex.captureGroups(for: line)
73+
guard let group = TestCasePassedCaptureGroup(groups: groups) else { return nil }
7474
return TestCase(classname: group.suite, name: group.testCase, time: group.time)
7575
}
7676

7777
private func generateSkippedTest(line: String) -> TestCase? {
78-
guard let _group: any CaptureGroup = line.captureGroup(with: TestCaseSkippedCaptureGroup.pattern) else { return nil }
79-
guard let group = _group as? TestCaseSkippedCaptureGroup else { return nil }
78+
let groups = TestCaseSkippedCaptureGroup.regex.captureGroups(for: line)
79+
guard let group = TestCaseSkippedCaptureGroup(groups: groups) else { return nil }
8080
return TestCase(classname: group.suite, name: group.testCase, time: group.time, skipped: .init(message: nil))
8181
}
8282

8383
private func generatePassingParallelTest(line: String) -> TestCase? {
84-
guard let _group: any CaptureGroup = line.captureGroup(with: ParallelTestCasePassedCaptureGroup.pattern) else { return nil }
85-
guard let group = _group as? ParallelTestCasePassedCaptureGroup else { return nil }
84+
let groups = ParallelTestCasePassedCaptureGroup.regex.captureGroups(for: line)
85+
guard let group = ParallelTestCasePassedCaptureGroup(groups: groups) else { return nil }
8686
return TestCase(classname: group.suite, name: group.testCase, time: group.time)
8787
}
8888

8989
private func generateSkippedParallelTest(line: String) -> TestCase? {
90-
guard let _group: any CaptureGroup = line.captureGroup(with: ParallelTestCaseSkippedCaptureGroup.pattern) else { return nil }
91-
guard let group = _group as? ParallelTestCaseSkippedCaptureGroup else { return nil }
90+
let groups = ParallelTestCaseSkippedCaptureGroup.regex.captureGroups(for: line)
91+
guard let group = ParallelTestCaseSkippedCaptureGroup(groups: groups) else { return nil }
9292
return TestCase(classname: group.suite, name: group.testCase, time: group.time, skipped: .init(message: nil))
9393
}
9494

9595
private func generateSuiteStart(line: String) -> String? {
96-
guard let _group: any CaptureGroup = line.captureGroup(with: TestSuiteStartCaptureGroup.pattern) else { return nil }
97-
guard let group = _group as? TestSuiteStartCaptureGroup else { return nil }
96+
let groups = TestSuiteStartCaptureGroup.regex.captureGroups(for: line)
97+
guard let group = TestSuiteStartCaptureGroup(groups: groups) else { return nil }
9898
return group.testSuiteName
9999
}
100100

Sources/XcbeautifyLib/Parser.swift

+11-44
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
1-
package class Parser {
2-
private let colored: Bool
3-
4-
private let renderer: any OutputRendering
5-
6-
private let additionalLines: () -> String?
7-
8-
private let preserveUnbeautifiedLines: Bool
9-
10-
package private(set) var outputType = OutputType.undefined
1+
import Foundation
112

3+
package final class Parser {
124
private lazy var captureGroupTypes: [any CaptureGroup.Type] = [
135
AnalyzeCaptureGroup.self,
146
BuildTargetCaptureGroup.self,
@@ -104,57 +96,32 @@ package class Parser {
10496

10597
// MARK: - Init
10698

107-
package init(
108-
colored: Bool = true,
109-
renderer: Renderer,
110-
preserveUnbeautifiedLines: Bool = false,
111-
additionalLines: @escaping () -> (String?)
112-
) {
113-
self.colored = colored
114-
115-
switch renderer {
116-
case .terminal:
117-
self.renderer = TerminalRenderer(colored: colored, additionalLines: additionalLines)
118-
case .gitHubActions:
119-
self.renderer = GitHubActionsRenderer(colored: colored, additionalLines: additionalLines)
120-
case .teamcity:
121-
self.renderer = TeamCityRenderer(colored: colored, additionalLines: additionalLines)
122-
}
123-
124-
self.preserveUnbeautifiedLines = preserveUnbeautifiedLines
125-
self.additionalLines = additionalLines
126-
}
99+
package init() { }
127100

128-
package func parse(line: String) -> String? {
101+
package func parse(line: String) -> (any CaptureGroup)? {
129102
if line.isEmpty {
130-
outputType = .undefined
131103
return nil
132104
}
133105

134106
// Find first parser that can parse specified string
135107
guard let idx = captureGroupTypes.firstIndex(where: { $0.regex.match(string: line) }) else {
136-
// Some uncommon cases, which have additional logic and don't follow default flow
137-
138-
// Nothing found?
139-
outputType = OutputType.undefined
140-
return preserveUnbeautifiedLines ? line : nil
108+
return nil
141109
}
142110

143111
guard let captureGroupType = captureGroupTypes[safe: idx] else {
144112
assertionFailure()
145113
return nil
146114
}
147115

148-
let formattedOutput = renderer.beautify(
149-
line: line,
150-
pattern: captureGroupType.pattern
151-
)
152-
153-
outputType = captureGroupType.outputType
116+
let groups: [String] = captureGroupType.regex.captureGroups(for: line)
117+
guard let captureGroup = captureGroupType.init(groups: groups) else {
118+
assertionFailure()
119+
return nil
120+
}
154121

155122
// Move found parser to the top, so next time it will be checked first
156123
captureGroupTypes.insert(captureGroupTypes.remove(at: idx), at: 0)
157124

158-
return formattedOutput
125+
return captureGroup
159126
}
160127
}

Sources/XcbeautifyLib/Regex.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22

33
// `NSRegularExpression` is marked as `@unchecked Sendable`.
44
// Match the definition here.
5-
final class Regex: @unchecked Sendable {
5+
package final class Regex: @unchecked Sendable {
66
let pattern: String
77

88
private lazy var regex: NSRegularExpression? = {

0 commit comments

Comments
 (0)