Skip to content

[swiftSyntax] Swift side syntax classifier #18251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions test/IDE/coloring.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// We need to require macOS because swiftSyntax currently doesn't build on Linux
// REQUIRES: OS=macosx
// RUN: %target-swift-ide-test -syntax-coloring -source-filename %s | %FileCheck %s
// RUN: %target-swift-ide-test -syntax-coloring -typecheck -source-filename %s | %FileCheck %s -check-prefixes CHECK,CHECK-OLD
// RUN: %swift-swiftsyntax-test --classify-syntax --source-file %s | %FileCheck %s --check-prefixes CHECK,CHECK-NEW
// XFAIL: broken_std_regex

#line 17 "abc.swift"
Expand Down
3 changes: 3 additions & 0 deletions test/IDE/coloring_configs.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// We need to require macOS because swiftSyntax currently doesn't build on Linux
// REQUIRES: OS=macosx
// RUN: %target-swift-ide-test -syntax-coloring -source-filename %s -D CONF | %FileCheck %s
// RUN: %swift-swiftsyntax-test --classify-syntax --source-file %s | %FileCheck %s

// CHECK: <kw>var</kw> f : <type>Int</type>
var f : Int
Expand Down
3 changes: 3 additions & 0 deletions test/IDE/coloring_keywords.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// We need to require macOS because swiftSyntax currently doesn't build on Linux
// REQUIRES: OS=macosx
// RUN: %target-swift-ide-test -syntax-coloring -source-filename %s | %FileCheck %s
// RUN: %target-swift-ide-test -syntax-coloring -typecheck -source-filename %s | %FileCheck %s
// RUN: %swift-swiftsyntax-test --classify-syntax --source-file %s | %FileCheck %s

// CHECK: <kw>return</kw> c.return

Expand Down
3 changes: 3 additions & 0 deletions test/IDE/coloring_unclosed_hash_if.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// We need to require macOS because swiftSyntax currently doesn't build on Linux
// REQUIRES: OS=macosx
// RUN: %target-swift-ide-test -syntax-coloring -source-filename %s | %FileCheck %s
// RUN: %target-swift-ide-test -syntax-coloring -typecheck -source-filename %s | %FileCheck %s
// RUN: %swift-swiftsyntax-test --classify-syntax --source-file %s | %FileCheck %s

// CHECK: <#kw>#if</#kw> <#id>d</#id>
// CHECK-NEXT: <kw>func</kw> bar() {
Expand Down
6 changes: 6 additions & 0 deletions test/SwiftSyntax/AllTokenKindsInSyntaxGybSupport.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// RUN: %utils/gyb --line-directive '' %S/Inputs/TokenKindList.txt.gyb | sort > %T/python_kinds.txt
// RUN: %swift-syntax-test --dump-all-syntax-tokens | sort > %T/def_kinds.txt
// RUN: diff %T/def_kinds.txt %T/python_kinds.txt

// Check that all token kinds listed in TokenKinds.def are also in
// gyb_syntax_support/Token.py
6 changes: 6 additions & 0 deletions test/SwiftSyntax/Inputs/TokenKindList.txt.gyb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
%{
from gyb_syntax_support import *
}%
% for token in SYNTAX_TOKENS:
${token.kind}
% end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an action print-token-kind in swift-swiftsyntax-test for this output.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think that this way is better. This is not really swift-specific since we're just checking the list of declarations in the python declaration of gyb_syntax_support.

If we were to implement this in swift-swiftsyntax-test we would need to:
a) Make swift-swiftsyntax-test be a gyb tool (I don't like this at all)
b) Need to check that all the tokenKinds get generated in the TokenKind enum, but for that we'd need to have a list of all kinds define in TokenKind and we cannot use the autogenerated allCases property using CaseIterable since TokenKind has associated values.

And after all this file is not a real tool that needs to be compiled and is a standalone binary, but is just an input to gyb.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nkcsgexi and I discussed this in person and decided that it's probably the easiest and cleanest way to test this. In the future we might want to consider generation TokenKinds.def from gyb which would make the entire test obsolete.

1 change: 1 addition & 0 deletions test/lit.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ lit_config.note("Using code completion cache: " + completion_cache_path)
config.substitutions.append( ('%validate-incrparse', '%utils/incrparse/validate_parse.py --temp-dir %t --swift-syntax-test %swift-syntax-test') )
config.substitutions.append( ('%incr-transfer-tree', '%utils/incrparse/incr_transfer_tree.py --temp-dir %t --swift-syntax-test %swift-syntax-test') )
config.substitutions.append( ('%incr-transfer-roundtrip', '%%utils/incrparse/incr_transfer_round_trip.py --temp-dir %%t --swift-syntax-test %%swift-syntax-test --swift-swiftsyntax-test %r' % (config.swift_swiftsyntax_test)) )
config.substitutions.append( ('%swift-swiftsyntax-test', '%r' % config.swift_swiftsyntax_test))
config.substitutions.append( ('%swift_obj_root', config.swift_obj_root) )
config.substitutions.append( ('%swift_src_root', config.swift_src_root) )
config.substitutions.append( ('%{python}', sys.executable) )
Expand Down
1 change: 1 addition & 0 deletions tools/SwiftSyntax/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ add_swift_library(swiftSwiftSyntax SHARED
SyntaxData.swift
SyntaxChildren.swift
SyntaxCollections.swift.gyb
SyntaxClassifier.swift.gyb
SyntaxBuilders.swift.gyb
SyntaxFactory.swift.gyb
SyntaxKind.swift.gyb
Expand Down
3 changes: 0 additions & 3 deletions tools/SwiftSyntax/SwiftSyntax.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,6 @@ public enum SyntaxTreeParser {
public static func parse(_ url: URL) throws -> SourceFileSyntax {
let swiftcRunner = try SwiftcRunner(sourceFile: url)
let result = try swiftcRunner.invoke()
guard result.wasSuccessful else {
throw ParserError.swiftcFailed(result.exitCode, result.stderr)
}
let syntaxTreeData = result.stdoutData
let deserializer = SyntaxTreeDeserializer()
return try deserializer.deserialize(syntaxTreeData)
Expand Down
124 changes: 124 additions & 0 deletions tools/SwiftSyntax/SyntaxClassifier.swift.gyb
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
%{
from gyb_syntax_support import *
# -*- mode: Swift -*-
# Ignore the following admonition it applies to the resulting .swift file only
}%
//// Automatically Generated From SyntaxClassifier.swift.gyb.
//// Do Not Edit Directly!
//===------------ SyntaxClassifier.swift.gyb - Syntax Collection ----------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

public enum SyntaxClassification {
% for classification in SYNTAX_CLASSIFICATIONS:
% for line in dedented_lines(classification.description):
% if line:
/// ${line}
% end
% end
case ${classification.swift_name}
% end
}

class _SyntaxClassifier: SyntaxVisitor {

private var contextStack: [(classification: SyntaxClassification, force: Bool)] =
[(classification: .none, force: false)]

var classifications: [TokenSyntax: SyntaxClassification] = [:]

private func visit(
_ node: Syntax,
classification: SyntaxClassification,
force: Bool = false
) {
contextStack.append((classification: classification, force: force))
visit(node)
contextStack.removeLast()
}

private func getContextFreeClassificationForTokenKind(_ tokenKind: TokenKind)
-> SyntaxClassification? {
switch (tokenKind) {
% for token in SYNTAX_TOKENS:
case .${token.swift_kind()}:
% if token.classification:
return SyntaxClassification.${token.classification.swift_name}
% else:
return nil
% end
% end
case .eof:
return SyntaxClassification.none
}
}

override func visit(_ token: TokenSyntax) {
var classification = contextStack.last!.classification
if !contextStack.last!.force {
if let contextFreeClassification =
getContextFreeClassificationForTokenKind(token.tokenKind) {
classification = contextFreeClassification
}
if case .unknown = token.tokenKind, token.text.starts(with: "\"") {
classification = .stringLiteral
} else if case .identifier = token.tokenKind,
token.text.starts(with: "<#") && token.text.hasSuffix("#>") {
classification = .editorPlaceholder
}
}
assert(classifications[token] == nil)
classifications[token] = classification
}

% for node in SYNTAX_NODES:
% if is_visitable(node):
override func visit(_ node: ${node.name}) {
% if node.is_unknown() or node.is_syntax_collection():
super.visit(node)
% else:
% for child in node.children:
% if child.is_optional:
if let ${child.swift_name} = node.${child.swift_name} {
% if child.classification:
visit(${child.swift_name},
classification: .${child.classification.swift_name},
force: ${"true" if child.force_classification else "false"})
% else:
visit(${child.swift_name})
% end
}
% else:
% if child.classification:
visit(node.${child.swift_name},
classification: .${child.classification.swift_name},
force: ${"true" if child.force_classification else "false"})
% else:
visit(node.${child.swift_name})
% end
% end
% end
% end

}
% end
% end
}

public enum SyntaxClassifier {
/// Classify all tokens in the given syntax tree for syntax highlighting
public static func classifyTokensInTree(_ syntaxTree: SourceFileSyntax)
-> [TokenSyntax: SyntaxClassification] {
let classifier = _SyntaxClassifier()
classifier.visit(syntaxTree)
return classifier.classifications
}
}
1 change: 1 addition & 0 deletions tools/swift-swiftsyntax-test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
add_swift_host_tool(swift-swiftsyntax-test
main.swift
ClassifiedSyntaxTreePrinter.swift
CommandLineArguments.swift
empty.c # FIXME: If there is no C file in the target Xcode skips the linking phase and doesn't create the executable
COMPILE_FLAGS "-module-name" "main"
Expand Down
107 changes: 107 additions & 0 deletions tools/swift-swiftsyntax-test/ClassifiedSyntaxTreePrinter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import SwiftSyntax
import Foundation

class ClassifiedSyntaxTreePrinter: SyntaxVisitor {
private let classifications: [TokenSyntax: SyntaxClassification]
private var currentTag = ""
private var skipNextNewline = false
private var result = ""

// MARK: Public interface

init(classifications: [TokenSyntax: SyntaxClassification]) {
self.classifications = classifications
}

func print(tree: SourceFileSyntax) -> String {
result = ""
visit(tree)
// Emit the last closing tag
recordCurrentTag("")
return result
}

// MARK: Implementation

/// Closes the current tag if it is different from the previous one and opens
/// a tag with the specified ID.
private func recordCurrentTag(_ tag: String) {
if currentTag != tag {
if !currentTag.isEmpty {
result += "</" + currentTag + ">"
}
if !tag.isEmpty {
result += "<" + tag + ">"
}
}
currentTag = tag
}

private func visit(_ piece: TriviaPiece) {
let tag: String
switch piece {
case .spaces, .tabs, .verticalTabs, .formfeeds:
tag = ""
case .newlines, .carriageReturns, .carriageReturnLineFeeds:
if skipNextNewline {
skipNextNewline = false
return
}
tag = ""
case .backticks:
tag = ""
case .lineComment(let text):
// Don't print CHECK lines
if text.hasPrefix("// CHECK") {
skipNextNewline = true
return
}
tag = "comment-line"
case .blockComment:
tag = "comment-block"
case .docLineComment:
tag = "doc-comment-line"
case .docBlockComment:
tag = "doc-comment-block"
case .garbageText:
tag = ""
}
recordCurrentTag(tag)
piece.write(to: &result)
}

private func visit(_ trivia: Trivia) {
for piece in trivia {
visit(piece)
}
}

private func getTagForSyntaxClassification(
_ classification: SyntaxClassification
) -> String {
switch (classification) {
case .none: return ""
case .keyword: return "kw"
case .identifier: return ""
case .typeIdentifier: return "type"
case .dollarIdentifier: return "dollar"
case .integerLiteral: return "int"
case .floatingLiteral: return "float"
case .stringLiteral: return "str"
case .stringInterpolationAnchor: return "anchor"
case .poundDirectiveKeyword: return "#kw"
case .buildConfigId: return "#id"
case .attribute: return "attr-builtin"
case .objectLiteral: return "object-literal"
case .editorPlaceholder: return "placeholder"
}
}

override func visit(_ node: TokenSyntax) {
visit(node.leadingTrivia)
let classification = classifications[node] ?? SyntaxClassification.none
recordCurrentTag(getTagForSyntaxClassification(classification))
result += node.text
visit(node.trailingTrivia)
}
}
25 changes: 23 additions & 2 deletions tools/swift-swiftsyntax-test/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@ func printHelp() {
incrementally transferred post-edit syntax tree (--incr-tree) and
write the source representation of the post-edit syntax tree to an
out file (--out).
--classify-syntax
Parse the given source file (--source-file) and output it with
tokens classified for syntax colouring.
--help
Print this help message

Arguments:
--source-file FILENAME
The path to a Swift source file to parse
--pre-edit-tree FILENAME
The path to a JSON serialized pre-edit syntax tree
--incr-tree FILENAME
Expand All @@ -47,22 +52,38 @@ func performRoundTrip(args: CommandLineArguments) throws {
try sourceRepresenation.write(to: outURL, atomically: false, encoding: .utf8)
}

func performClassifySyntax(args: CommandLineArguments) throws {
let treeURL = URL(fileURLWithPath: try args.getRequired("--source-file"))

let tree = try SyntaxTreeParser.parse(treeURL)
let classifications = SyntaxClassifier.classifyTokensInTree(tree)
let printer = ClassifiedSyntaxTreePrinter(classifications: classifications)
let result = printer.print(tree: tree)

if let outURL = args["--out"].map(URL.init(fileURLWithPath:)) {
try result.write(to: outURL, atomically: false, encoding: .utf8)
} else {
print(result)
}
}

do {
let args = try CommandLineArguments.parse(CommandLine.arguments.dropFirst())

if args.has("--deserialize-incremental") {
try performRoundTrip(args: args)
exit(0)
} else if args.has("--classify-syntax") {
try performClassifySyntax(args: args)
} else if args.has("--help") {
printHelp()
exit(0)
} else {
printerr("""
No action specified.
See --help for information about available actions
""")
exit(1)
}
exit(0)
} catch {
printerr(error.localizedDescription)
printerr("Run swift-swiftsyntax-test --help for more help.")
Expand Down
Loading