Skip to content

Commit

Permalink
[File] Fixed out-of-bounds exception and inaccurate parsed declaratio…
Browse files Browse the repository at this point in the history
…ns (fixes #35)
  • Loading branch information
jpsim committed Mar 26, 2015
1 parent 9622e6b commit 73941ec
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 71 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ None.
[JP Simard](https://github.com/jpsim)
[#30](https://github.com/jpsim/SourceKitten/issues/30)

* Fixed out-of-bounds exception and inaccurate parsed declarations when using
multibyte characters.
[JP Simard](https://github.com/jpsim)
[#35](https://github.com/jpsim/SourceKitten/issues/35)


## 0.3.0

##### Breaking
Expand Down
55 changes: 31 additions & 24 deletions Source/SourceKittenFramework/File.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ public struct File {
public let path: String?
/// File contents.
public let contents: NSString
/// Array of newline offsets in contents.
public let lineBreaks: [Int]

/**
Failable initializer by path. Fails if file contents could not be read as a UTF8 string.
Expand All @@ -27,7 +25,6 @@ public struct File {
self.path = path
if let contents = NSString(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: nil) {
self.contents = contents
lineBreaks = contents.lineBreaks()
} else {
fputs("Could not read contents of `\(path)`\n", stderr)
return nil
Expand All @@ -42,7 +39,6 @@ public struct File {
public init(contents: NSString) {
path = nil
self.contents = contents
lineBreaks = contents.lineBreaks()
}

/**
Expand All @@ -52,18 +48,28 @@ public struct File {
:returns: Source declaration if successfully parsed.
*/
private func parseDeclaration(dictionary: XPCDictionary) -> String? {
public func parseDeclaration(dictionary: XPCDictionary) -> String? {
if !shouldParseDeclaration(dictionary) {
return nil
}
let offset = Int(SwiftDocKey.getOffset(dictionary)!)
let lineBreakIndexBeforeOffset = indexBefore(offset, inSequence: lineBreaks) ?? 0
let previousLineBreakOffset = lineBreaks[lineBreakIndexBeforeOffset] + 1
if let bodyOffset = SwiftDocKey.getBodyOffset(dictionary) {
return contents.filteredSubstring(previousLineBreakOffset, end: Int(bodyOffset))
if let offset = flatMap(SwiftDocKey.getOffset(dictionary), {Int($0)}) {
let stringContents = contents as String
let bodyOffsetIndex = flatMap(SwiftDocKey.getBodyOffset(dictionary)) {
advance(stringContents.utf8.startIndex, Int($0))
}
var lineStart = stringContents.startIndex
var lineEnd = stringContents.endIndex
let declarationStart = advance(stringContents.utf8.startIndex, offset).samePositionIn(stringContents)!
let declarationEnd = (bodyOffsetIndex ?? advance(stringContents.utf8.startIndex, offset+1)).samePositionIn(stringContents)!
stringContents.getLineStart(&lineStart,
end: &lineEnd,
contentsEnd: nil,
forRange: declarationStart..<declarationEnd)
let unwantedSet = NSCharacterSet.whitespaceAndNewlineCharacterSet().mutableCopy() as! NSMutableCharacterSet
unwantedSet.addCharactersInString("{")
return stringContents[lineStart..<lineEnd].stringByTrimmingCharactersInSet(unwantedSet)
}
let nextLineBreakOffset = (lineBreakIndexBeforeOffset + 1 < lineBreaks.count) ? lineBreaks[lineBreakIndexBeforeOffset + 1] : lineBreaks.last!
return contents.filteredSubstring(previousLineBreakOffset, end: nextLineBreakOffset + 1)
return nil
}

/**
Expand Down Expand Up @@ -250,21 +256,22 @@ public struct File {
internal func shouldTreatAsSameFile(dictionary: XPCDictionary) -> Bool {
return path == SwiftDocKey.getFilePath(dictionary)
}
}

/**
Returns true if the input dictionary contains a parseable declaration.
/**
Returns true if the input dictionary contains a parseable declaration.
:param: dictionary Dictionary to parse.
:param: dictionary Dictionary to parse.
:returns: True if the input dictionary contains a parseable declaration.
*/
private func shouldParseDeclaration(dictionary: XPCDictionary) -> Bool {
let hasTypeName = SwiftDocKey.getTypeName(dictionary) != nil
let hasAnnotatedDeclaration = SwiftDocKey.getAnnotatedDeclaration(dictionary) != nil
let hasOffset = SwiftDocKey.getOffset(dictionary) != nil
let isntExtension = SwiftDocKey.getKind(dictionary) != SwiftDeclarationKind.Extension.rawValue
return hasTypeName && hasAnnotatedDeclaration && hasOffset && isntExtension
:returns: True if the input dictionary contains a parseable declaration.
*/
private func shouldParseDeclaration(dictionary: XPCDictionary) -> Bool {
let sameFile = shouldTreatAsSameFile(dictionary)
let hasTypeName = SwiftDocKey.getTypeName(dictionary) != nil
let hasAnnotatedDeclaration = SwiftDocKey.getAnnotatedDeclaration(dictionary) != nil
let hasOffset = SwiftDocKey.getOffset(dictionary) != nil
let isntExtension = SwiftDocKey.getKind(dictionary) != SwiftDeclarationKind.Extension.rawValue
return sameFile && hasTypeName && hasAnnotatedDeclaration && hasOffset && isntExtension
}
}

/**
Expand Down
35 changes: 0 additions & 35 deletions Source/SourceKittenFramework/NSString+SourceKitten.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,41 +35,6 @@ extension NSString {
return NSString.pathWithComponents([rootDirectory, self]).stringByStandardizingPath
}

/**
Returns offsets of all the line breaks in the current string.
*/
public func lineBreaks() -> [Int] {
var lineBreaks = [-1] // Count start of file
var searchRange = NSRange(location: 0, length: length)
while searchRange.length > 0 {
searchRange.length = length - searchRange.location
let foundRange = rangeOfString("\n", options: nil, range: searchRange)
if foundRange.location != NSNotFound {
lineBreaks.append(foundRange.location)
searchRange.location = foundRange.location + foundRange.length
} else {
break
}
}
return lineBreaks
}

/**
Filter self from start to end while trimming unwanted characters (whitespace & '{').
:param: start Starting offset to filter.
:param: end Ending offset to filter.
:returns: Filtered string.
*/
public func filteredSubstring(start: Int, end: Int) -> String {
precondition(end > start, "Range should be positive")
let range = NSRange(location: start, length: end - start - 1)
let unwantedSet = NSCharacterSet.whitespaceAndNewlineCharacterSet().mutableCopy() as! NSMutableCharacterSet
unwantedSet.addCharactersInString("{")
return substringWithRange(range).stringByTrimmingCharactersInSet(unwantedSet)
}

/**
Find integer offsets of documented Swift tokens in self.
Expand Down
8 changes: 4 additions & 4 deletions Source/SourceKittenFrameworkTests/Fixtures/Commandant.json
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@
{
"key.kind" : "source.lang.swift.decl.var.static",
"key.offset" : 3222,
"key.parsed_declaration" : "static var name: String",
"key.parsed_declaration" : "static var name: String { get }",
"key.namelength" : 4,
"key.bodyoffset" : 3236,
"key.bodylength" : 5,
Expand Down Expand Up @@ -1126,7 +1126,7 @@
{
"key.kind" : "source.lang.swift.decl.var.instance",
"key.offset" : 384,
"key.parsed_declaration" : "var verb: String",
"key.parsed_declaration" : "var verb: String { get }",
"key.namelength" : 4,
"key.bodyoffset" : 398,
"key.bodylength" : 5,
Expand All @@ -1151,7 +1151,7 @@
{
"key.kind" : "source.lang.swift.decl.var.instance",
"key.offset" : 496,
"key.parsed_declaration" : "var function: String",
"key.parsed_declaration" : "var function: String { get }",
"key.namelength" : 8,
"key.bodyoffset" : 514,
"key.bodylength" : 5,
Expand Down Expand Up @@ -1329,7 +1329,7 @@
{
"key.kind" : "source.lang.swift.decl.function.method.instance",
"key.offset" : 1225,
"key.parsed_declaration" : "public init()",
"key.parsed_declaration" : "public init() {}",
"key.namelength" : 6,
"key.bodyoffset" : 1233,
"key.bodylength" : 0,
Expand Down
19 changes: 11 additions & 8 deletions Source/SourceKittenFrameworkTests/StringTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import Foundation
import SourceKittenFramework
import SwiftXPC
import XCTest

class StringTests: XCTestCase {
Expand Down Expand Up @@ -42,15 +43,17 @@ class StringTests: XCTestCase {
XCTAssertEqual(__FILE__.absolutePathRepresentation(), __FILE__, "absolutePathRepresentation() should return the caller if it's already an absolute path")
}

func testParseLineBreaks() {
XCTAssertEqual("a\nbc\nd\n".lineBreaks(), [-1, 1, 4, 6], "should parse line breaks")
}

func testFilteredSubstring() {
let expected = "public func myFunc()"
let end = count(expected) + 4 // 4 == 2 spaces before + 2 characters after (until newline)
let actual = (" \(expected) {\n}" as NSString).filteredSubstring(0, end: end)
XCTAssertEqual(expected, actual, "should extract function declaration from source text")
let dict = [
"key.kind": "source.lang.swift.decl.class",
"key.offset": Int64(24),
"key.bodyoffset": Int64(32),
"key.annotated_decl": "",
"key.typename": "ClassA.Type"
] as XPCDictionary
// This string is a regression test for https://github.com/jpsim/SourceKitten/issues/35.
let file = File(contents: "/**\n ほげ\n*/\nclass ClassA {\n}\n")
XCTAssertEqual("class ClassA", file.parseDeclaration(dict)!, "should extract declaration from source text")
}

func testGenerateDocumentedTokenOffsets() {
Expand Down

0 comments on commit 73941ec

Please sign in to comment.