Skip to content

Commit 7b64925

Browse files
authored
Merge pull request swiftlang#144 from dylansturg/safer_unknown_nodes
Reject invalid files & use the same DiagnosticEngine for all diagnostics
2 parents 0266e97 + a65a7e3 commit 7b64925

File tree

10 files changed

+227
-46
lines changed

10 files changed

+227
-46
lines changed

Package.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ let package = Package(
7171
"SwiftToolsSupport-auto",
7272
]
7373
),
74+
.testTarget(
75+
name: "SwiftFormatTests",
76+
dependencies: [
77+
"SwiftFormat",
78+
]
79+
),
7480
.testTarget(
7581
name: "SwiftFormatConfigurationTests",
7682
dependencies: ["SwiftFormatConfiguration"]

Sources/SwiftFormat/SwiftFormatError.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,7 @@ public enum SwiftFormatError: Error {
1818

1919
/// The requested file was a directory.
2020
case isDirectory
21+
22+
/// The file contains invalid or unrecognized Swift syntax and cannot be handled safely.
23+
case fileContainsInvalidSyntax
2124
}

Sources/SwiftFormat/SwiftFormatter.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,11 @@ public final class SwiftFormatter {
9191
public func format<Output: TextOutputStream>(
9292
syntax: SourceFileSyntax, assumingFileURL url: URL?, to outputStream: inout Output
9393
) throws {
94-
let assumedURL = url ?? URL(fileURLWithPath: "source")
94+
guard isSyntaxValidForProcessing(Syntax(syntax)) else {
95+
throw SwiftFormatError.fileContainsInvalidSyntax
96+
}
9597

98+
let assumedURL = url ?? URL(fileURLWithPath: "source")
9699
let context = Context(
97100
configuration: configuration, diagnosticEngine: diagnosticEngine, fileURL: assumedURL,
98101
sourceFileSyntax: syntax)

Sources/SwiftFormat/SwiftLinter.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ public final class SwiftLinter {
7575
/// - url: A file URL denoting the filename/path that should be assumed for this syntax tree.
7676
/// - Throws: If an unrecoverable error occurs when formatting the code.
7777
public func lint(syntax: SourceFileSyntax, assumingFileURL url: URL) throws {
78+
guard isSyntaxValidForProcessing(Syntax(syntax)) else {
79+
throw SwiftFormatError.fileContainsInvalidSyntax
80+
}
81+
7882
let context = Context(
7983
configuration: configuration, diagnosticEngine: diagnosticEngine, fileURL: url,
8084
sourceFileSyntax: syntax)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
15+
/// A SyntaxVisitor that searches for nodes that cannot be handled safely.
16+
fileprivate class SyntaxValidatingVisitor: SyntaxVisitor {
17+
/// Tracks whether an invalid node has been encountered.
18+
var isValidSyntax = true
19+
20+
override func visit(_ node: UnknownSyntax) -> SyntaxVisitorContinueKind {
21+
isValidSyntax = false
22+
return .skipChildren
23+
}
24+
25+
override func visit(_ node: UnknownDeclSyntax) -> SyntaxVisitorContinueKind {
26+
isValidSyntax = false
27+
return .skipChildren
28+
}
29+
30+
override func visit(_ node: UnknownExprSyntax) -> SyntaxVisitorContinueKind {
31+
isValidSyntax = false
32+
return .skipChildren
33+
}
34+
35+
override func visit(_ node: UnknownStmtSyntax) -> SyntaxVisitorContinueKind {
36+
isValidSyntax = false
37+
return .skipChildren
38+
}
39+
40+
override func visit(_ node: UnknownTypeSyntax) -> SyntaxVisitorContinueKind {
41+
isValidSyntax = false
42+
return .skipChildren
43+
}
44+
45+
override func visit(_ node: UnknownPatternSyntax) -> SyntaxVisitorContinueKind {
46+
isValidSyntax = false
47+
return .skipChildren
48+
}
49+
50+
override func visit(_ node: NonEmptyTokenListSyntax) -> SyntaxVisitorContinueKind {
51+
isValidSyntax = false
52+
return .skipChildren
53+
}
54+
55+
override func visit(_ node: AttributeSyntax) -> SyntaxVisitorContinueKind {
56+
// The token list is used to collect any unexpected tokens. When it's missing or empty, then
57+
// there were no unexpected tokens. Otherwise, the attribute is invalid.
58+
guard node.tokenList?.isEmpty ?? true else {
59+
isValidSyntax = false
60+
return .skipChildren
61+
}
62+
return .visitChildren
63+
}
64+
}
65+
66+
/// Returns whether the given syntax contains any nodes which are invalid or unrecognized and
67+
/// cannot be handled safely.
68+
///
69+
/// - Parameter syntax: The root of a tree of syntax nodes to check for compatibility.
70+
func isSyntaxValidForProcessing(_ syntax: Syntax) -> Bool {
71+
let visitor = SyntaxValidatingVisitor()
72+
visitor.walk(syntax)
73+
return visitor.isValidSyntax
74+
}

Sources/swift-format/Run.swift

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,40 +25,38 @@ import TSCBasic
2525
/// - sourceFile: A file handle from which to read the source code to be linted.
2626
/// - assumingFilename: The filename of the source file, used in diagnostic output.
2727
/// - debugOptions: The set containing any debug options that were supplied on the command line.
28+
/// - diagnosticEngine: A diagnostic collector that handles diagnostic messages.
2829
/// - Returns: Zero if there were no lint errors, otherwise a non-zero number.
2930
func lintMain(
3031
configuration: Configuration, sourceFile: FileHandle, assumingFilename: String?,
31-
debugOptions: DebugOptions
32+
debugOptions: DebugOptions, diagnosticEngine: DiagnosticEngine
3233
) -> Int {
33-
let diagnosticEngine = makeDiagnosticEngine()
3434
let linter = SwiftLinter(configuration: configuration, diagnosticEngine: diagnosticEngine)
3535
linter.debugOptions = debugOptions
3636
let assumingFileURL = URL(fileURLWithPath: assumingFilename ?? "<stdin>")
3737

3838
guard let source = readSource(from: sourceFile) else {
39-
stderrStream.write("Unable to read source for linting from \(assumingFileURL.path).\n")
40-
stderrStream.flush()
39+
diagnosticEngine.diagnose(
40+
Diagnostic.Message(.error, "Unable to read source for linting from \(assumingFileURL.path)."))
4141
return 1
4242
}
4343

4444
do {
4545
try linter.lint(source: source, assumingFileURL: assumingFileURL)
4646
} catch SwiftFormatError.fileNotReadable {
4747
let path = assumingFileURL.path
48-
stderrStream.write("Unable to lint \(path): file is not readable or does not exist.\n")
49-
stderrStream.flush()
48+
diagnosticEngine.diagnose(
49+
Diagnostic.Message(.error, "Unable to lint \(path): file is not readable or does not exist."))
5050
return 1
51-
} catch {
51+
} catch SwiftFormatError.fileContainsInvalidSyntax {
52+
let path = assumingFileURL.path
53+
diagnosticEngine.diagnose(
54+
Diagnostic.Message(
55+
.error, "Unable to line \(path): file contains invalid or unrecognized Swift syntax."))
56+
return 1
57+
} catch {
5258
let path = assumingFileURL.path
53-
// Workaround: we're unable to directly catch unknownTokenKind errors due to access
54-
// restrictions. TODO: this can be removed when we update to Swift 5.0.
55-
if "\(error)" == "unknownTokenKind(\"pound_error\")" {
56-
stderrStream.write("Unable to lint \(path): unknownTokenKind(\"pound_error\")\n")
57-
stderrStream.flush()
58-
return 1
59-
}
60-
stderrStream.write("Unable to lint \(path): \(error)\n")
61-
stderrStream.flush()
59+
diagnosticEngine.diagnose(Diagnostic.Message(.error, "Unable to lint \(path): \(error)"))
6260
exit(1)
6361
}
6462
return diagnosticEngine.diagnostics.isEmpty ? 0 : 1
@@ -72,18 +70,20 @@ func lintMain(
7270
/// - assumingFilename: The filename of the source file, used in diagnostic output.
7371
/// - inPlace: Whether or not to overwrite the current file when formatting.
7472
/// - debugOptions: The set containing any debug options that were supplied on the command line.
73+
/// - diagnosticEngine: A diagnostic collector that handles diagnostic messages.
7574
/// - Returns: Zero if there were no format errors, otherwise a non-zero number.
7675
func formatMain(
7776
configuration: Configuration, sourceFile: FileHandle, assumingFilename: String?, inPlace: Bool,
78-
debugOptions: DebugOptions
77+
debugOptions: DebugOptions, diagnosticEngine: DiagnosticEngine
7978
) -> Int {
80-
let formatter = SwiftFormatter(configuration: configuration, diagnosticEngine: nil)
79+
let formatter = SwiftFormatter(configuration: configuration, diagnosticEngine: diagnosticEngine)
8180
formatter.debugOptions = debugOptions
8281
let assumingFileURL = URL(fileURLWithPath: assumingFilename ?? "<stdin>")
8382

8483
guard let source = readSource(from: sourceFile) else {
85-
stderrStream.write("Unable to read source for formatting from \(assumingFileURL.path).\n")
86-
stderrStream.flush()
84+
diagnosticEngine.diagnose(
85+
Diagnostic.Message(
86+
.error, "Unable to read source for formatting from \(assumingFileURL.path)."))
8787
return 1
8888
}
8989

@@ -103,26 +103,24 @@ func formatMain(
103103
}
104104
} catch SwiftFormatError.fileNotReadable {
105105
let path = assumingFileURL.path
106-
stderrStream.write("Unable to format \(path): file is not readable or does not exist.\n")
107-
stderrStream.flush()
106+
diagnosticEngine.diagnose(
107+
Diagnostic.Message(
108+
.error, "Unable to format \(path): file is not readable or does not exist."))
109+
return 1
110+
} catch SwiftFormatError.fileContainsInvalidSyntax {
111+
let path = assumingFileURL.path
112+
diagnosticEngine.diagnose(
113+
Diagnostic.Message(
114+
.error, "Unable to format \(path): file contains invalid or unrecognized Swift syntax."))
108115
return 1
109116
} catch {
110117
let path = assumingFileURL.path
111-
stderrStream.write("Unable to format \(path): \(error)\n")
112-
stderrStream.flush()
118+
diagnosticEngine.diagnose(Diagnostic.Message(.error, "Unable to format \(path): \(error)"))
113119
exit(1)
114120
}
115121
return 0
116122
}
117123

118-
/// Makes and returns a new configured diagnostic engine.
119-
fileprivate func makeDiagnosticEngine() -> DiagnosticEngine {
120-
let engine = DiagnosticEngine()
121-
let consumer = PrintingDiagnosticConsumer()
122-
engine.addConsumer(consumer)
123-
return engine
124-
}
125-
126124
/// Reads from the given file handle until EOF is reached, then returns the contents as a UTF8
127125
/// encoded string.
128126
fileprivate func readSource(from fileHandle: FileHandle) -> String? {

Sources/swift-format/main.swift

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ import Foundation
1414
import SwiftFormat
1515
import SwiftFormatConfiguration
1616
import SwiftFormatCore
17+
import SwiftSyntax
1718
import TSCBasic
1819
import TSCUtility
1920

2021
fileprivate func main(_ arguments: [String]) -> Int32 {
2122
let url = URL(fileURLWithPath: arguments.first!)
2223
let options = processArguments(commandName: url.lastPathComponent, Array(arguments.dropFirst()))
24+
let diagnosticEngine = makeDiagnosticEngine()
2325
switch options.mode {
2426
case .format:
2527
if options.paths.isEmpty {
@@ -29,13 +31,17 @@ fileprivate func main(_ arguments: [String]) -> Int32 {
2931
formatMain(
3032
configuration: configuration, sourceFile: FileHandle.standardInput,
3133
assumingFilename: options.assumeFilename, inPlace: false,
32-
debugOptions: options.debugOptions))
34+
debugOptions: options.debugOptions, diagnosticEngine: diagnosticEngine))
3335
}
34-
return processSources(from: options.paths, configurationPath: options.configurationPath) {
36+
return processSources(
37+
from: options.paths, configurationPath: options.configurationPath,
38+
diagnosticEngine: diagnosticEngine
39+
) {
3540
(sourceFile, path, configuration) in
3641
formatMain(
3742
configuration: configuration, sourceFile: sourceFile, assumingFilename: path,
38-
inPlace: options.inPlace, debugOptions: options.debugOptions)
43+
inPlace: options.inPlace, debugOptions: options.debugOptions,
44+
diagnosticEngine: diagnosticEngine)
3945
}
4046
case .lint:
4147
if options.paths.isEmpty {
@@ -44,13 +50,17 @@ fileprivate func main(_ arguments: [String]) -> Int32 {
4450
return Int32(
4551
lintMain(
4652
configuration: configuration, sourceFile: FileHandle.standardInput,
47-
assumingFilename: options.assumeFilename, debugOptions: options.debugOptions))
53+
assumingFilename: options.assumeFilename, debugOptions: options.debugOptions,
54+
diagnosticEngine: diagnosticEngine))
4855
}
49-
return processSources(from: options.paths, configurationPath: options.configurationPath) {
56+
return processSources(
57+
from: options.paths, configurationPath: options.configurationPath,
58+
diagnosticEngine: diagnosticEngine
59+
) {
5060
(sourceFile, path, configuration) in
5161
lintMain(
5262
configuration: configuration, sourceFile: sourceFile, assumingFilename: path,
53-
debugOptions: options.debugOptions)
63+
debugOptions: options.debugOptions, diagnosticEngine: diagnosticEngine)
5464
}
5565
case .dumpConfiguration:
5666
dumpDefaultConfiguration()
@@ -66,16 +76,17 @@ fileprivate func main(_ arguments: [String]) -> Int32 {
6676
/// - Parameters:
6777
/// - paths: The file paths for the source files to process with a transformation.
6878
/// - configurationPath: The file path to a swift-format configuration file.
79+
/// - diagnosticEngine: A diagnostic collector that handles diagnostic messages.
6980
/// - transform: A closure that performs a transformation on a specific source file.
7081
fileprivate func processSources(
71-
from paths: [String], configurationPath: String?,
82+
from paths: [String], configurationPath: String?, diagnosticEngine: DiagnosticEngine,
7283
transform: (FileHandle, String, Configuration) -> Int
7384
) -> Int32 {
7485
var result = 0
7586
for path in FileIterator(paths: paths) {
7687
guard let sourceFile = FileHandle(forReadingAtPath: path) else {
77-
stderrStream.write("Unable to create a file handle for source from \(path).\n")
78-
stderrStream.flush()
88+
diagnosticEngine.diagnose(
89+
Diagnostic.Message(.error, "Unable to create a file handle for source from \(path)."))
7990
return 1
8091
}
8192
let configuration = loadConfiguration(forSwiftFile: path, configFilePath: configurationPath)
@@ -84,6 +95,14 @@ fileprivate func processSources(
8495
return Int32(result)
8596
}
8697

98+
/// Makes and returns a new configured diagnostic engine.
99+
fileprivate func makeDiagnosticEngine() -> DiagnosticEngine {
100+
let engine = DiagnosticEngine()
101+
let consumer = PrintingDiagnosticConsumer()
102+
engine.addConsumer(consumer)
103+
return engine
104+
}
105+
87106
/// Load the configuration.
88107
fileprivate func loadConfiguration(
89108
forSwiftFile swiftFilePath: String?, configFilePath: String?
@@ -92,15 +111,15 @@ fileprivate func loadConfiguration(
92111
return decodedConfiguration(fromFile: URL(fileURLWithPath: configFilePath))
93112
}
94113

95-
if let swiftFileURL = swiftFilePath.map(URL.init(fileURLWithPath:)),
96-
let configFileURL = Configuration.url(forConfigurationFileApplyingTo: swiftFileURL) {
97-
return decodedConfiguration(fromFile: configFileURL)
114+
if let swiftFileURL = swiftFilePath.map(URL.init(fileURLWithPath:)),
115+
let configFileURL = Configuration.url(forConfigurationFileApplyingTo: swiftFileURL)
116+
{
117+
return decodedConfiguration(fromFile: configFileURL)
98118
}
99119

100120
return Configuration()
101121
}
102122

103-
104123
/// Loads and returns a `Configuration` from the given JSON file if it is found and is valid. If the
105124
/// file does not exist or there was an error decoding it, the program exits with a non-zero exit
106125
/// code.

Tests/LinuxMain.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import SwiftFormatConfigurationTests
44
import SwiftFormatCoreTests
55
import SwiftFormatPrettyPrintTests
66
import SwiftFormatRulesTests
7+
import SwiftFormatTests
78
import SwiftFormatWhitespaceLinterTests
89

910
var tests = [XCTestCaseEntry]()
1011
tests += SwiftFormatConfigurationTests.__allTests()
1112
tests += SwiftFormatCoreTests.__allTests()
1213
tests += SwiftFormatPrettyPrintTests.__allTests()
1314
tests += SwiftFormatRulesTests.__allTests()
15+
tests += SwiftFormatTests.__allTests()
1416
tests += SwiftFormatWhitespaceLinterTests.__allTests()
1517

1618
XCTMain(tests)

0 commit comments

Comments
 (0)