SwiftParse.swift 4.85 KB
Newer Older
Drew's avatar
Drew committed
1 2 3 4 5 6 7 8 9 10 11 12 13
// Copyright (c) 2016 Drew Crawford.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
Drew's avatar
Drew committed
14

Drew's avatar
Drew committed
15 16
import StandBack

Drew's avatar
Drew committed
17 18 19
//bug: don't support unicode
private let identifierPattern = "[[:alnum:]]+"

20
func findTests(_ sourceFiles: [String]) -> [String] {
Drew's avatar
Drew committed
21 22
    var tests: [String] = []
    for file in sourceFiles {
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
      //load source text

      let sourceText: String
      do {
        sourceText = try String(file: file)
      }
      catch {
        switch error {

          //if (and only if) the missing file is CarolineXCTest, we continue
          //this is because a missing file is generally a build error
          //however CarolineXCTest is often deliberately not checked in, to avoid conflicts between
          //atllbuild `**` behavior and Xcode's generation behavior
          //if we die here without CarolineXCTest, we will fail to regenerate the file as expected
          case caroline_static_tool.FileUtilsError.OpenError where file.hasSuffix("CarolineXCTest.swift"):
          continue
          default:
          fatalError("\(error)")
        }
      }
Drew's avatar
Drew committed
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
      tests.append(contentsOf: findTests(sourceText: sourceText))
    }

    return tests.sorted(by: {$0 < $1})
}

private struct NamedScope {
  let name: String
  let start: Int
  let end: Int
  let underlyingString: String
  var description: String {
      let utf8 = self.underlyingString.utf8
      return String(describing: utf8[utf8.index(utf8.startIndex, offsetBy: start)..<utf8.index(utf8.startIndex, offsetBy:end)])
  }
  //[0]: type
  //[1]: identifier
Drew's avatar
Drew committed
60
  private static let namedScopeBeginRegex = try! Regex(pattern: "(class|enum|struct|extension)[[:space:]]+(\(identifierPattern))[^{]*\\{")
Drew's avatar
Drew committed
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
  static func parse(sourceText: String) -> [NamedScope] {
    var scopes: [NamedScope] = []
    for namedScope in NamedScope.namedScopeBeginRegex.findAll(inString: sourceText) {
      if (scopes.last?.end ?? 0) > namedScope.entireMatch.start { continue } //scope is not "flat"
      let scopeStart = namedScope.entireMatch.end
      let scopeChomp = sourceText.utf8.substring(from: scopeStart)
      var indent = 0
      var scopeExitPosition: Int? = nil
      loop:
      for (idx, utf) in scopeChomp.utf8.enumerated() {
        switch(utf) {
        case "}".utf8.first!:
          indent -= 1
          if indent == -1 {
            scopeExitPosition = idx
            break loop
          }
        case "{".utf8.first!:
          indent += 1
        default:
          break
Drew's avatar
Drew committed
82
        }
Drew's avatar
Drew committed
83 84 85 86
      }
      guard let scopeExit = scopeExitPosition else { fatalError("Parse error")}
      let s = NamedScope(name: namedScope.groups[1]!.description, start: scopeStart, end: scopeExit + scopeStart, underlyingString: sourceText)
      scopes.append(s)
Drew's avatar
Drew committed
87
    }
Drew's avatar
Drew committed
88 89
    return scopes
  }
Drew's avatar
Drew committed
90

Drew's avatar
Drew committed
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
  func overlaps(_ match: Match) -> Bool {
    if self.start < match.end && self.end < match.end { return false }
    if self.start > match.end && self.end > match.end { return false }
    return true
  }
}

func findTests(sourceText: String, namespace: String? = nil) -> [String] {
      var tests: [String] = []

      let scopes = NamedScope.parse(sourceText: sourceText)
      for scope in scopes {
        let fqdn: String
        if let n = namespace {
          fqdn = "\(n).\(scope.name)"
        }
        else {
          fqdn = scope.name
        }
        tests.append(contentsOf: findTests(sourceText: scope.description, namespace: fqdn))
      }
      for match in try! Regex(pattern: "class[[:space:]]+(\(identifierPattern))[[:space:]]*:[[:space:]]*CarolineTest[[:space:]]*\\{").findAll(inString: sourceText) {
          let className = match.groups[0]!.description
          //is this match inside one of our scopes?
          let matchingScopes = scopes.filter({$0.overlaps(match.entireMatch) && className != $0.name})
          if matchingScopes.count > 0 { 
            continue 
          }
          let fqdn: String
          if let n = namespace {
            fqdn = "\(n).\(className)"
          }
          else {
            fqdn = className
          }
          tests.append(fqdn)
      }
      return tests
Drew's avatar
Drew committed
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
}

func allTestsDeclaration(tests: [String], indentation: Int) -> String {

  var s = ""
  for _ in 0..<indentation {s += " "}
  s += "let allTests: [CarolineTest] = [\n"
  var i = 0
  for test in tests {
    if i != 0 { s += ",\n" }
    i += 1
    for _ in 0..<indentation+4 {s += " "}
    s += "\(test)()"
  }
  s += "\n"
  for _ in 0..<indentation {s += " "}
  s += "]"
  return s
}