Skip to content

Commit 70a543a

Browse files
authored
Merge pull request swiftlang#153 from dylansturg/ignore_files
Support an ignore directive to ignore an entire file.
2 parents 41e0bd2 + 5848303 commit 70a543a

File tree

7 files changed

+266
-4
lines changed

7 files changed

+266
-4
lines changed

Documentation/IgnoringSource.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@ technical limitations of the formatter's implementation. When an ignore comment
66
is present, the next ["node"](#understanding-nodes) in the source's AST
77
representation is ignored by the formatter.
88

9+
## Ignore A File
10+
11+
In the event that an entire file cannot be formatted, add a comment that contains
12+
`swift-format-ignore-file` at the top of the file and the formatter will leave
13+
the file completely unchanged.
14+
15+
```swift
16+
// swift-format-ignore-file
17+
import Zoo
18+
import Arrays
19+
20+
struct Foo {
21+
func foo() { bar();baz(); }
22+
}
23+
```
24+
925
## Ignoring Formatting (aka indentation, line breaks, line length, etc.)
1026

1127
The formatter applies line length to add line breaks and indentation throughout

Sources/SwiftFormatCore/RuleMask.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ import SwiftSyntax
2222
/// 2. | let a = 123
2323
/// Ignores all rules for line 2.
2424
///
25+
/// 1. | // swift-format-ignore-file
26+
/// 2. | let a = 123
27+
/// 3. | class Foo { }
28+
/// Ignores all rules for an entire file (lines 2-3).
29+
///
2530
/// 1. | // swift-format-ignore: RuleName
2631
/// 2. | let a = 123
2732
/// Ignores `RuleName` for line 2.
@@ -108,6 +113,12 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor {
108113
/// Rule ignore regex object.
109114
private let ignoreRegex: NSRegularExpression
110115

116+
/// Regex pattern to match an ignore comment that applies to an entire file.
117+
private let ignoreFilePattern = #"^\s*\/\/\s*swift-format-ignore-file$"#
118+
119+
/// Rule ignore regex object.
120+
private let ignoreFileRegex: NSRegularExpression
121+
111122
/// Stores the source ranges in which all rules are ignored.
112123
var allRulesIgnoredRanges: [SourceRange] = []
113124

@@ -116,12 +127,36 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor {
116127

117128
init(sourceLocationConverter: SourceLocationConverter) {
118129
ignoreRegex = try! NSRegularExpression(pattern: ignorePattern, options: [])
130+
ignoreFileRegex = try! NSRegularExpression(pattern: ignoreFilePattern, options: [])
119131

120132
self.sourceLocationConverter = sourceLocationConverter
121133
}
122134

123135
// MARK: - Syntax Visitation Methods
124136

137+
override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind {
138+
guard let firstToken = node.firstToken else {
139+
return .visitChildren
140+
}
141+
let comments = loneLineComments(in: firstToken.leadingTrivia, isFirstToken: true)
142+
var foundIgnoreFileComment = false
143+
for comment in comments {
144+
let range = NSRange(comment.startIndex..<comment.endIndex, in: comment)
145+
if ignoreFileRegex.firstMatch(in: comment, options: [], range: range) != nil {
146+
foundIgnoreFileComment = true
147+
break
148+
}
149+
}
150+
guard foundIgnoreFileComment else {
151+
return .visitChildren
152+
}
153+
154+
let sourceRange = node.sourceRange(
155+
converter: sourceLocationConverter, afterLeadingTrivia: false, afterTrailingTrivia: true)
156+
allRulesIgnoredRanges.append(sourceRange)
157+
return .skipChildren
158+
}
159+
125160
override func visit(_ node: CodeBlockItemSyntax) -> SyntaxVisitorContinueKind {
126161
guard let firstToken = node.firstToken else {
127162
return .visitChildren

Sources/SwiftFormatPrettyPrint/TokenStreamCreator.swift

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,10 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor {
11431143
}
11441144

11451145
override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind {
1146+
if shouldFormatterIgnore(file: node) {
1147+
appendFormatterIgnored(node: Syntax(node))
1148+
return .skipChildren
1149+
}
11461150
after(node.eofToken, tokens: .break(.same, newlines: .soft))
11471151
return .visitChildren
11481152
}
@@ -3191,6 +3195,13 @@ class CommentMovingRewriter: SyntaxRewriter {
31913195
/// Map of tokens to alternate trivia to use as the token's leading trivia.
31923196
var rewriteTokenTriviaMap: [TokenSyntax: Trivia] = [:]
31933197

3198+
override func visit(_ node: SourceFileSyntax) -> Syntax {
3199+
if shouldFormatterIgnore(file: node) {
3200+
return Syntax(node)
3201+
}
3202+
return super.visit(node)
3203+
}
3204+
31943205
override func visit(_ node: CodeBlockItemSyntax) -> Syntax {
31953206
if shouldFormatterIgnore(node: Syntax(node)) {
31963207
return Syntax(node)
@@ -3280,14 +3291,18 @@ extension TriviaPiece {
32803291

32813292
/// Returns whether the given trivia includes a directive to ignore formatting for the next node.
32823293
///
3283-
/// - Parameter trivia: Leading trivia for a node that the formatter supports ignoring.
3284-
fileprivate func isFormatterIgnorePresent(inTrivia trivia: Trivia) -> Bool {
3294+
/// - Parameters:
3295+
/// - trivia: Leading trivia for a node that the formatter supports ignoring.
3296+
/// - isWholeFile: Whether to search for a whole-file ignore directive or per node ignore.
3297+
/// - Returns: Whether the trivia contains the specified type of ignore directive.
3298+
fileprivate func isFormatterIgnorePresent(inTrivia trivia: Trivia, isWholeFile: Bool) -> Bool {
32853299
func isFormatterIgnore(in commentText: String, prefix: String, suffix: String) -> Bool {
32863300
let trimmed =
32873301
commentText.dropFirst(prefix.count)
32883302
.dropLast(suffix.count)
32893303
.trimmingCharacters(in: .whitespaces)
3290-
return trimmed == "swift-format-ignore"
3304+
let pattern = isWholeFile ? "swift-format-ignore-file" : "swift-format-ignore"
3305+
return trimmed == pattern
32913306
}
32923307

32933308
for piece in trivia {
@@ -3317,7 +3332,19 @@ fileprivate func shouldFormatterIgnore(node: Syntax) -> Bool {
33173332
// Regardless of the level of nesting, if the ignore directive is present on the first token
33183333
// contained within the node then the entire node is eligible for ignoring.
33193334
if let firstTrivia = node.firstToken?.leadingTrivia {
3320-
return isFormatterIgnorePresent(inTrivia: firstTrivia)
3335+
return isFormatterIgnorePresent(inTrivia: firstTrivia, isWholeFile: false)
3336+
}
3337+
return false
3338+
}
3339+
3340+
/// Returns whether the formatter should ignore the given file by printing it without changing the
3341+
/// any if its nodes' internal text representation (i.e. print all text inside of the file as it was
3342+
/// in the original source).
3343+
///
3344+
/// - Parameter file: The root syntax node for a source file.
3345+
fileprivate func shouldFormatterIgnore(file: SourceFileSyntax) -> Bool {
3346+
if let firstTrivia = file.firstToken?.leadingTrivia {
3347+
return isFormatterIgnorePresent(inTrivia: firstTrivia, isWholeFile: true)
33213348
}
33223349
return false
33233350
}

Tests/SwiftFormatCoreTests/RuleMaskTests.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,62 @@ final class RuleMaskTests: XCTestCase {
171171
XCTAssertEqual(mask.ruleState("TotallyMadeUpRule", at: location(ofLine: 3)), .default)
172172
XCTAssertEqual(mask.ruleState("rule1", at: location(ofLine: 4)), .default)
173173
}
174+
175+
func testAllRulesWholeFileIgnore() {
176+
let text =
177+
"""
178+
// This file has important contents.
179+
// swift-format-ignore-file
180+
// Everything in this file is ignored.
181+
182+
let a = 5
183+
let b = 4
184+
185+
class Foo {
186+
let member1 = 0
187+
func foo() {
188+
baz()
189+
}
190+
}
191+
192+
struct Baz {
193+
}
194+
"""
195+
196+
let mask = createMask(sourceText: text)
197+
198+
let lineCount = text.split(separator: "\n").count
199+
for i in 0..<lineCount {
200+
XCTAssertEqual(mask.ruleState("rule1", at: location(ofLine: i)), .disabled)
201+
}
202+
}
203+
204+
func testAllRulesWholeFileIgnoreNestedInNode() {
205+
let text =
206+
"""
207+
// This file has important contents.
208+
// Everything in this file is ignored.
209+
210+
let a = 5
211+
let b = 4
212+
213+
class Foo {
214+
// swift-format-ignore-file
215+
let member1 = 0
216+
func foo() {
217+
baz()
218+
}
219+
}
220+
221+
struct Baz {
222+
}
223+
"""
224+
225+
let mask = createMask(sourceText: text)
226+
227+
let lineCount = text.split(separator: "\n").count
228+
for i in 0..<lineCount {
229+
XCTAssertEqual(mask.ruleState("rule1", at: location(ofLine: i)), .default)
230+
}
231+
}
174232
}

Tests/SwiftFormatCoreTests/XCTestManifests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ extension RuleMaskTests {
66
// `swift test --generate-linuxmain`
77
// to regenerate.
88
static let __allTests__RuleMaskTests = [
9+
("testAllRulesWholeFileIgnore", testAllRulesWholeFileIgnore),
10+
("testAllRulesWholeFileIgnoreNestedInNode", testAllRulesWholeFileIgnoreNestedInNode),
911
("testDirectiveWithRulesList", testDirectiveWithRulesList),
1012
("testDuplicateNested", testDuplicateNested),
1113
("testIgnoreTwoRules", testIgnoreTwoRules),

Tests/SwiftFormatPrettyPrintTests/IgnoreNodeTests.swift

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,4 +289,126 @@ final class IgnoreNodeTests: PrettyPrintTestCase {
289289

290290
assertPrettyPrintEqual(input: input, expected: expected, linelength: 50)
291291
}
292+
293+
func testIgnoreWholeFile() {
294+
let input =
295+
"""
296+
// swift-format-ignore-file
297+
import Zoo
298+
import Aoo
299+
import foo
300+
301+
struct Foo {
302+
private var baz: Bool {
303+
return foo +
304+
bar + // poorly placed comment
305+
false
306+
}
307+
308+
var a = true // line comment
309+
// aligned line comment
310+
var b = false // correct trailing comment
311+
312+
var c = 0 +
313+
1
314+
+ (2 + 3)
315+
}
316+
317+
class Bar
318+
{
319+
var bazzle = 0 }
320+
"""
321+
322+
let expected =
323+
"""
324+
// swift-format-ignore-file
325+
import Zoo
326+
import Aoo
327+
import foo
328+
329+
struct Foo {
330+
private var baz: Bool {
331+
return foo +
332+
bar + // poorly placed comment
333+
false
334+
}
335+
336+
var a = true // line comment
337+
// aligned line comment
338+
var b = false // correct trailing comment
339+
340+
var c = 0 +
341+
1
342+
+ (2 + 3)
343+
}
344+
345+
class Bar
346+
{
347+
var bazzle = 0 }
348+
"""
349+
350+
assertPrettyPrintEqual(input: input, expected: expected, linelength: 50)
351+
}
352+
353+
func testIgnoreWholeFileInNestedNode() {
354+
let input =
355+
"""
356+
import Zoo
357+
import Aoo
358+
import foo
359+
360+
// swift-format-ignore-file
361+
struct Foo {
362+
private var baz: Bool {
363+
return foo +
364+
bar + // poorly placed comment
365+
false
366+
}
367+
368+
var a = true // line comment
369+
// aligned line comment
370+
var b = false // correct trailing comment
371+
372+
var c = 0 +
373+
1
374+
+ (2 + 3)
375+
}
376+
377+
class Bar
378+
{
379+
// swift-format-ignore-file
380+
var bazzle = 0 }
381+
"""
382+
383+
let expected =
384+
"""
385+
import Zoo
386+
import Aoo
387+
import foo
388+
389+
// swift-format-ignore-file
390+
struct Foo {
391+
private var baz: Bool {
392+
return foo + bar // poorly placed comment
393+
+ false
394+
}
395+
396+
var a = true // line comment
397+
// aligned line comment
398+
var b = false // correct trailing comment
399+
400+
var c =
401+
0 + 1
402+
+ (2 + 3)
403+
}
404+
405+
class Bar {
406+
// swift-format-ignore-file
407+
var bazzle = 0
408+
}
409+
410+
"""
411+
412+
assertPrettyPrintEqual(input: input, expected: expected, linelength: 50)
413+
}
292414
}

Tests/SwiftFormatPrettyPrintTests/XCTestManifests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,8 @@ extension IgnoreNodeTests {
396396
("testIgnoreInvalidAfterFirstToken", testIgnoreInvalidAfterFirstToken),
397397
("testIgnoreMemberDeclListItems", testIgnoreMemberDeclListItems),
398398
("testIgnoresNestedMembers", testIgnoresNestedMembers),
399+
("testIgnoreWholeFile", testIgnoreWholeFile),
400+
("testIgnoreWholeFileInNestedNode", testIgnoreWholeFileInNestedNode),
399401
("testInvalidComment", testInvalidComment),
400402
("testValidComment", testValidComment),
401403
]

0 commit comments

Comments
 (0)