Skip to content

Commit ba4ad8e

Browse files
Add Xcode source extension
1 parent 5eea3ce commit ba4ad8e

File tree

15 files changed

+955
-0
lines changed

15 files changed

+955
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.app-sandbox</key>
6+
<true/>
7+
</dict>
8+
</plist>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>NSExtension</key>
6+
<dict>
7+
<key>NSExtensionAttributes</key>
8+
<dict>
9+
<key>XCSourceEditorCommandDefinitions</key>
10+
<array>
11+
<dict>
12+
<key>XCSourceEditorCommandClassName</key>
13+
<string>$(PRODUCT_MODULE_NAME).SourceEditorCommand</string>
14+
<key>XCSourceEditorCommandIdentifier</key>
15+
<string>$(PRODUCT_BUNDLE_IDENTIFIER).SourceEditorCommand</string>
16+
<key>XCSourceEditorCommandName</key>
17+
<string>Source Editor Command</string>
18+
</dict>
19+
</array>
20+
<key>XCSourceEditorExtensionPrincipalClass</key>
21+
<string>$(PRODUCT_MODULE_NAME).SourceEditorExtension</string>
22+
</dict>
23+
<key>NSExtensionPointIdentifier</key>
24+
<string>com.apple.dt.Xcode.extension.source-editor</string>
25+
</dict>
26+
</dict>
27+
</plist>
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import Foundation
2+
import XcodeKit
3+
import SwiftSyntax
4+
import SwiftParser
5+
6+
private let nilLiteral = ExprSyntax(NilLiteralExprSyntax())
7+
private let falseLiteral = ExprSyntax(BooleanLiteralExprSyntax(literal: .keyword(.false)))
8+
9+
class SourceEditorCommand: NSObject, XCSourceEditorCommand {
10+
func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {
11+
let selections = invocation.buffer.selections as? [XCSourceTextRange] ?? []
12+
13+
let sourceFile = Parser.parse(source: invocation.buffer.completeBuffer)
14+
let locationConverter = SourceLocationConverter(fileName: "", tree: sourceFile)
15+
16+
let rewriter = PowerAssertRewriter(
17+
selections: selections,
18+
locationConverter: locationConverter
19+
)
20+
invocation.buffer.completeBuffer = rewriter.rewrite(sourceFile).description
21+
22+
completionHandler(nil)
23+
}
24+
}
25+
26+
class PowerAssertRewriter: SyntaxRewriter {
27+
let selections: [XCSourceTextRange]
28+
let locationConverter: SourceLocationConverter
29+
30+
init(selections: [XCSourceTextRange], locationConverter: SourceLocationConverter) {
31+
self.selections = selections
32+
self.locationConverter = locationConverter
33+
34+
super.init()
35+
}
36+
37+
override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax {
38+
guard node.isContainedIn(selections: selections, locationConverter: locationConverter) else {
39+
return super.visit(node)
40+
}
41+
42+
if let calledExpression = node.calledExpression.as(DeclReferenceExprSyntax.self) {
43+
switch calledExpression.baseName.tokenKind {
44+
case .identifier("XCTAssert"):
45+
return super.visit(node.rewriteAssertTrue())
46+
case .identifier("XCTAssertEqual"):
47+
if node.arguments.dropFirst(2).first?.label?.tokenKind == .identifier("accuracy") {
48+
return super.visit(node)
49+
}
50+
return super.visit(node.rewriteComparisonExpression(op: "=="))
51+
case .identifier("XCTAssertFalse"):
52+
return super.visit(node.rewriteAssertWithExpression(falseLiteral))
53+
case .identifier("XCTAssertGreaterThan"):
54+
return super.visit(node.rewriteComparisonExpression(op: ">"))
55+
case .identifier("XCTAssertGreaterThanOrEqual"):
56+
return super.visit(node.rewriteComparisonExpression(op: ">="))
57+
case .identifier("XCTAssertIdentical"):
58+
return super.visit(node.rewriteComparisonExpression(op: "==="))
59+
case .identifier("XCTAssertLessThan"):
60+
return super.visit(node.rewriteComparisonExpression(op: "<"))
61+
case .identifier("XCTAssertLessThanOrEqual"):
62+
return super.visit(node.rewriteComparisonExpression(op: "<="))
63+
case .identifier("XCTAssertNil"):
64+
return super.visit(node.rewriteAssertWithExpression(nilLiteral))
65+
case .identifier("XCTAssertNotEqual"):
66+
if node.arguments.dropFirst(2).first?.label?.tokenKind == .identifier("accuracy") {
67+
return super.visit(node)
68+
}
69+
return super.visit(node.rewriteComparisonExpression(op: "!="))
70+
case .identifier("XCTAssertNotIdentical"):
71+
return super.visit(node.rewriteComparisonExpression(op: "!=="))
72+
case .identifier("XCTAssertTrue"):
73+
return super.visit(node.rewriteAssertTrue())
74+
default:
75+
break
76+
}
77+
}
78+
return super.visit(node)
79+
}
80+
}
81+
82+
extension FunctionCallExprSyntax {
83+
func rewriteAssertTrue() -> Self {
84+
return with(
85+
\.calledExpression, ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("#assert")))
86+
)
87+
.with(\.leadingTrivia, leadingTrivia)
88+
}
89+
90+
func rewriteAssertWithExpression(_ expression: ExprSyntax) -> Self {
91+
guard arguments.count >= 1 else {
92+
return self
93+
}
94+
95+
let first = arguments[arguments.startIndex]
96+
let remaining = arguments.dropFirst(1)
97+
98+
var arguments = LabeledExprListSyntax(
99+
arrayLiteral: LabeledExprSyntax(
100+
expression: InfixOperatorExprSyntax(
101+
leftOperand: TupleExprSyntax(elements: LabeledExprListSyntax(arrayLiteral: LabeledExprSyntax(expression: first.expression))),
102+
operator: BinaryOperatorExprSyntax(operator: .binaryOperator("=="))
103+
.with(\.leadingTrivia, .space)
104+
.with(\.trailingTrivia, .space),
105+
rightOperand: expression
106+
),
107+
trailingComma: remaining.isEmpty ? .identifier("") : .commaToken(trailingTrivia: .space)
108+
)
109+
)
110+
111+
for argument in remaining {
112+
arguments.insert(argument, at: arguments.endIndex)
113+
}
114+
115+
return with(
116+
\.calledExpression, ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("#assert")))
117+
)
118+
.with(\.arguments, arguments)
119+
.with(\.leadingTrivia, leadingTrivia)
120+
}
121+
122+
func rewriteComparisonExpression(op: String) -> Self {
123+
guard arguments.count >= 2 else {
124+
return self
125+
}
126+
127+
let first = arguments[arguments.startIndex]
128+
let second = arguments[arguments.index(arguments.startIndex, offsetBy: 1)]
129+
130+
let remainingArguments = arguments.dropFirst(2)
131+
132+
var arguments = LabeledExprListSyntax(
133+
arrayLiteral: LabeledExprSyntax(
134+
expression: InfixOperatorExprSyntax(
135+
leftOperand: first.expression,
136+
operator: BinaryOperatorExprSyntax(operator: .binaryOperator(op))
137+
.with(\.leadingTrivia, .space)
138+
.with(\.trailingTrivia, .space),
139+
rightOperand: second.expression
140+
),
141+
trailingComma: remainingArguments.isEmpty ? .identifier("") : .commaToken(trailingTrivia: .space)
142+
)
143+
)
144+
145+
for argument in remainingArguments {
146+
arguments.insert(argument, at: arguments.endIndex)
147+
}
148+
149+
return with(
150+
\.calledExpression, ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("#assert")))
151+
)
152+
.with(\.arguments, arguments)
153+
.with(\.leadingTrivia, leadingTrivia)
154+
}
155+
}
156+
157+
extension SyntaxProtocol {
158+
func isContainedIn(selections: [XCSourceTextRange], locationConverter: SourceLocationConverter) -> Bool {
159+
if selections.allSatisfy({ $0.start.line == $0.end.line && $0.start.column == $0.end.column }) {
160+
return true
161+
}
162+
163+
if !selections.isEmpty {
164+
for selection in selections {
165+
let startLocation = startLocation(converter: locationConverter)
166+
let endLocation = endLocation(converter: locationConverter)
167+
168+
let startLine = startLocation.line - 1
169+
let endLine = endLocation.line - 1
170+
171+
let startColumn = locationConverter.sourceLines[startLocation.line].prefix(startLocation.column).utf16.count
172+
let endColumn = locationConverter.sourceLines[endLocation.line].prefix(endLocation.column).utf16.count
173+
174+
if startLine >= selection.start.line && endLine <= selection.end.line {
175+
if startLine == selection.start.line && startColumn < selection.start.column {
176+
continue
177+
}
178+
if endLine == selection.end.line && endColumn > selection.end.column {
179+
continue
180+
}
181+
return true
182+
} else {
183+
continue
184+
}
185+
}
186+
}
187+
188+
return false
189+
}
190+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import Foundation
2+
import XcodeKit
3+
4+
class SourceEditorExtension: NSObject, XCSourceEditorExtension {}

0 commit comments

Comments
 (0)