diff --git a/.gitignore b/.gitignore index ecf248e..5e83637 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ build *.DS_Store !vendor/.gitignore - +compile_commands.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 39ea33b..63c9282 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,11 +98,11 @@ add_executable(http_server HTTPServerMain.cpp ) -add_executable(test_driver +add_executable(tests Logging.hpp SwiftCompleter.hpp SwiftCompleter.cpp - Driver.cpp + UnitTests.cpp ) add_executable(integration_tests diff --git a/Driver.cpp b/Driver.cpp deleted file mode 100644 index 156c3a1..0000000 --- a/Driver.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#import "Logging.hpp" -#import "SwiftCompleter.hpp" -#import -#import -#import - -using namespace ssvim; -using namespace std; - -struct Runner { - std::string complete(std::string fileName, std::string fileContents, - std::vector flags, unsigned line, - unsigned column) { - auto completer = SwiftCompleter(LogLevelExtreme); - auto files = std::vector(); - auto unsavedFile = UnsavedFile(); - unsavedFile.contents = fileContents; - unsavedFile.fileName = fileName; - - files.push_back(unsavedFile); - auto result = completer.CandidatesForLocationInFile(fileName, line, column, - files, flags); - - return result; - } -}; - -std::string contents = "// \n\ -// some_swift.swift \n\ -// Swift Completer \n\ -// \n\ -// Created by Jerry Marino on 4/30/16. \n\ -// Copyright © 2016 Jerry Marino. All rights reserved. \n\ -// \n\ - \n\ - \n\ - func someOtherFunc(){ \n\ - } \n\ - \n\ - func anotherFunction(){ \n\ - someOther()\n\ - } \n\ -\n"; - -using namespace ssvim; -static ssvim::Logger logger(LogLevelInfo); - -int wrapped_main() { - Runner runner; - std::cout << contents; - vector flags; - flags.push_back("-sdk"); - flags.push_back("/Applications/Xcode.app/Contents/Developer/Platforms/" - "MacOSX.platform/Developer/SDKs/MacOSX.sdk"); - flags.push_back("-target"); - flags.push_back("x86_64-apple-macosx10.12"); - - auto exampleFilePath = "/tmp/x"; - auto result = runner.complete(exampleFilePath, contents, flags, 19, 13); - logger << result; - logger << "Done"; - exit(0); -} - -int main() { - logger << "Running Test Driver"; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ - wrapped_main(); - }); - dispatch_main(); -} diff --git a/Examples/iOS/Basic/Basic/AppDelegate.swift b/Examples/iOS/Basic/Basic/AppDelegate.swift new file mode 100644 index 0000000..bc010ca --- /dev/null +++ b/Examples/iOS/Basic/Basic/AppDelegate.swift @@ -0,0 +1,26 @@ +// +// AppDelegate.swift +// Basic +// +// Created by Jerry Marino on 5/13/17. +// Copyright © 2017 Jerry Marino. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func ycmdMethod() { + self.window?.sub + } +} + diff --git a/Examples/iOS/Basic/Basic/ViewController.swift b/Examples/iOS/Basic/Basic/ViewController.swift new file mode 100644 index 0000000..c093681 --- /dev/null +++ b/Examples/iOS/Basic/Basic/ViewController.swift @@ -0,0 +1,24 @@ +// +// ViewController.swift +// Basic +// +// Created by Jerry Marino on 5/13/17. +// Copyright © 2017 Jerry Marino. All rights reserved. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + let delegate = UIApplication.shared.delegate as! AppDelegate + // Here we are calling a custom method on the AppDelegate + delegate.ycmd + } +} + diff --git a/Examples/iOS/Basic/README.md b/Examples/iOS/Basic/README.md new file mode 100644 index 0000000..efcf343 --- /dev/null +++ b/Examples/iOS/Basic/README.md @@ -0,0 +1,21 @@ +# YCMD iOS Swift Example + +This directory contains a basic example of an iOS project running under YCMD. + +The example contains a project generated from Xcode, minus any unneeded files ( +assets, xcodeproj, etc ) + +It's a simple iOS app and ViewController.swift depends on AppDelegate.swift. + +It should serve as a base case so that we can make sure symbols are correctly +loading for people from the iOS SDK and external files. + +These abilities are mostly dependent on a correct Compilation Database. + +The Compilation Database is a template which is written in test setup. It must +be in the root directory of the examples. This template was generated from a +build of these files under xcodebuild, with the following manual changes: + +- replace the source root with __SRCROOT__ +- remove 10.3 version requirement +- strip out flags that pass missing build artifacts diff --git a/Examples/iOS/Basic/compile_commands.json.template b/Examples/iOS/Basic/compile_commands.json.template new file mode 100644 index 0000000..e775cc5 --- /dev/null +++ b/Examples/iOS/Basic/compile_commands.json.template @@ -0,0 +1,12 @@ +[ +{ + "directory": "__SRCROOT__", + "command": "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -c -primary-file __SRCROOT__/Basic/ViewController.swift __SRCROOT__/Basic/AppDelegate.swift -enable-objc-interop -sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -enable-testing -g -serialize-debugging-options -Xcc -I/Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Intermediates/Basic.build/Debug-iphonesimulator/Basic.build/swift-overrides.hmap -Xcc -iquote -Xcc /Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Intermediates/Basic.build/Debug-iphonesimulator/Basic.build/Basic-generated-files.hmap -Xcc -I/Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Intermediates/Basic.build/Debug-iphonesimulator/Basic.build/Basic-own-target-headers.hmap -Xcc -I/Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Intermediates/Basic.build/Debug-iphonesimulator/Basic.build/Basic-all-target-headers.hmap -Xcc -iquote -Xcc /Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Intermediates/Basic.build/Debug-iphonesimulator/Basic.build/Basic-project-headers.hmap -Xcc -I/Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Products/Debug-iphonesimulator/include -Xcc -I/Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Intermediates/Basic.build/Debug-iphonesimulator/Basic.build/DerivedSources/x86_64 -Xcc -I/Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Intermediates/Basic.build/Debug-iphonesimulator/Basic.build/DerivedSources -Xcc -DDEBUG=1 -Xcc -working-directory__SRCROOT__ -Onone -module-name Basic -o /Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Intermediates/Basic.build/Debug-iphonesimulator/Basic.build/Objects-normal/x86_64/ViewController.o", + "file": "__SRCROOT__/Basic/ViewController.swift" +}, +{ + "directory": "__SRCROOT__", + "command": "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -c __SRCROOT__/Basic/ViewController.swift -primary-file __SRCROOT__/Basic/AppDelegate.swift -enable-objc-interop -sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -enable-testing -g -serialize-debugging-options -Xcc -I/Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Intermediates/Basic.build/Debug-iphonesimulator/Basic.build/swift-overrides.hmap -Xcc -iquote -Xcc /Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Intermediates/Basic.build/Debug-iphonesimulator/Basic.build/Basic-generated-files.hmap -Xcc -I/Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Intermediates/Basic.build/Debug-iphonesimulator/Basic.build/Basic-own-target-headers.hmap -Xcc -I/Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Intermediates/Basic.build/Debug-iphonesimulator/Basic.build/Basic-all-target-headers.hmap -Xcc -iquote -Xcc /Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Intermediates/Basic.build/Debug-iphonesimulator/Basic.build/Basic-project-headers.hmap -Xcc -I/Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Products/Debug-iphonesimulator/include -Xcc -I/Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Intermediates/Basic.build/Debug-iphonesimulator/Basic.build/DerivedSources/x86_64 -Xcc -I/Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Intermediates/Basic.build/Debug-iphonesimulator/Basic.build/DerivedSources -Xcc -DDEBUG=1 -Xcc -working-directory__SRCROOT__ -Onone -module-name Basic -o /Users/fakeuser/Library/Developer/Xcode/DerivedData/Basic-dcvyhoqxsulylretnajwiqnkngaj/Build/Intermediates/Basic.build/Debug-iphonesimulator/Basic.build/Objects-normal/x86_64/AppDelegate.o", + "file": "__SRCROOT__/Basic/AppDelegate.swift" +} +] diff --git a/SwiftCompleter.cpp b/SwiftCompleter.cpp index ca7f5ae..9a457bf 100644 --- a/SwiftCompleter.cpp +++ b/SwiftCompleter.cpp @@ -5,6 +5,7 @@ #import #import #import +#import #import #import #import @@ -449,7 +450,7 @@ const std::string SwiftCompleter::CandidatesForLocationInFile( ctx.line = line; ctx.column = column; ctx.unsavedFiles = unsavedFiles; - ctx.flags = flags; + ctx.flags = FlagsForCompileCommand(flags); SourceKitService sktService(_logger.level()); char *response = NULL; @@ -458,6 +459,21 @@ const std::string SwiftCompleter::CandidatesForLocationInFile( return response; } +// Transform completion flags into diagnostic flags +auto DiagnosticFlagsFromFlags(std::string filename, + std::vector flags) { + std::vector outputFlags; + // Skip the file - I'm not 100% sure why we need this but it will output + // weird warnings if not. + for (auto &f : flags) { + if (f == filename) { + continue; + } + outputFlags.push_back(f); + } + return outputFlags; +} + const std::string SwiftCompleter::DiagnosticsForFile(const std::string &filename, const std::vector &unsavedFiles, @@ -465,7 +481,8 @@ SwiftCompleter::DiagnosticsForFile(const std::string &filename, CompletionContext ctx; ctx.sourceFilename = filename; ctx.unsavedFiles = unsavedFiles; - ctx.flags = flags; + auto completionFlags = FlagsForCompileCommand(flags); + ctx.flags = DiagnosticFlagsFromFlags(filename, completionFlags); ctx.line = 0; ctx.column = 0; @@ -483,3 +500,75 @@ SwiftCompleter::DiagnosticsForFile(const std::string &filename, return semaresult; } } // namespace ssvim +// namespace ssvim + +#pragma mark - Command Preparation Logic + +// Basic flag blacklist is a list of flags that cannot be included in a +// CompilerInvocation for completion. +// +// These flags are a pair in the form +// __FLAG__ Optional(__VALUE__) +// +// For example -c sometimes has a value after it. +// +// Only skip __VALUE__ when it doesn't start with -. + +auto FlagStartToken = '-'; +std::set BasicFlagBlacklist{"-c", + "-MP", + "-MD", + "-MMD", + "--fcolor-diagnostics", + "-emit-reference-dependencies-path", + "-emit-dependencies-path", + "-emit-module-path", + "-serialize-diagnostics-path", + "-emit-module-doc-path", + "-frontend", + "-o"}; + +// These flags may specified as pair of the form +// __FLAG__ __VALUE__ +// +// Unconditionally exclude flags in this blacklist and the next value + +std::set PairedFlagBlacklist{"-Xcc"}; + +// Take a raw split command and output completion flags +std::vector +ssvim::FlagsForCompileCommand(std::vector flags) { + if (flags.size() == 0) { + return flags; + } + + std::vector outFlags; + + // Skip the first flag if needed + // Assume that someone will be using a swift compiler with an absolute path + // and strip it off if so. All compile commands should be formed this way, + // but some old test code uses it, so leave it for now. + auto isCompilerBinary = flags.at(0)[0] == '/'; + unsigned long i = isCompilerBinary ? 1 : 0; + auto length = flags.size(); + while (i < length) { + auto flag = flags.at(i); + if (PairedFlagBlacklist.find(flag) != PairedFlagBlacklist.end()) { + i = i + 1; + } else if (BasicFlagBlacklist.find(flag) != BasicFlagBlacklist.end()) { + auto nextIdx = i + 1; + + // Skip the pair (FLAG, VALUE) when the next value isn't + // another flag. + if (nextIdx < length) { + if (flags[nextIdx][0] != FlagStartToken) { + i = i + 1; + } + } + } else { + outFlags.push_back(flag); + } + i = i + 1; + } + return outFlags; +} diff --git a/SwiftCompleter.hpp b/SwiftCompleter.hpp index 00bf42f..4190197 100644 --- a/SwiftCompleter.hpp +++ b/SwiftCompleter.hpp @@ -39,4 +39,10 @@ class SwiftCompleter { const std::vector &unsavedFiles, const std::vector &flags); }; + +#pragma mark - Testing + +extern std::vector +FlagsForCompileCommand(std::vector flags); + } // namespace ssvim diff --git a/UnitTests.cpp b/UnitTests.cpp new file mode 100644 index 0000000..ae5e625 --- /dev/null +++ b/UnitTests.cpp @@ -0,0 +1,213 @@ +#import "Logging.hpp" +#import "SwiftCompleter.hpp" +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +using namespace ssvim; +using namespace std; + +std::string GetExamplesDir() { + char cwd[1024]; + if (getcwd(cwd, sizeof(cwd)) != NULL) { + return std::string(std::string(cwd) + "/Examples/"); + } + return ""; +} + +std::string ReadFile(const std::string &fileName) { + std::ifstream ifs(fileName.c_str(), + std::ios::in | std::ios::binary | std::ios::ate); + + std::ifstream::pos_type fileSize = ifs.tellg(); + ifs.seekg(0, std::ios::beg); + + std::vector bytes(fileSize); + ifs.read(&bytes[0], fileSize); + return std::string(&bytes[0], fileSize); +} +using boost::property_tree::ptree; +using boost::property_tree::read_json; +using boost::property_tree::write_json; + +ptree ReadJSON(std::string body) { + ptree pt; + std::istringstream is(body); + read_json(is, pt); + return pt; +} + +// Write out an example DB. +// Template the __SRCROOT__ with an absolute +// return the ptree value +ptree SetupTemplateCompilationDB() { + std::string exampleRoot = GetExamplesDir() + "iOS/Basic"; + auto exampleTemplate = + exampleRoot + "/" + std::string("compile_commands.json.template"); + std::ofstream outFile( + std::string(exampleRoot + "/" + std::string("compile_commands.json"))); + std::ifstream readFile(exampleTemplate); + std::string line; + std::string outStr; + while (std::getline(readFile, line)) { + boost::replace_all(line, std::string("__SRCROOT__"), exampleRoot); + outFile << line << std::endl; + outStr = outStr + line + "\n"; + } + outStr += "\0"; + auto JSON = ReadJSON(outStr); + return JSON; +} + +// Lex a shell invocation +template > C shlex(std::string s) { + auto result = C{}; + + auto accumulator = std::string{}; + auto quote = char{}; + auto escape = bool{}; + + auto evictAccumulator = [&]() { + if (!accumulator.empty()) { + result.push_back(std::move(accumulator)); + accumulator = ""; + } + }; + + for (auto c : s) { + if (escape) { + escape = false; + accumulator += c; + } else if (c == '\\') { + escape = true; + } else if ((quote == '\0' && c == '\'') || (quote == '\0' && c == '\"')) { + quote = c; + } else if ((quote == '\'' && c == '\'') || (quote == '"' && c == '"')) { + quote = '\0'; + } else if (!isspace(c) || quote != '\0') { + accumulator += c; + } else { + evictAccumulator(); + } + } + + evictAccumulator(); + + return result; +} + +auto BasicExampleNamed(std::string name) { + std::string exampleRoot = GetExamplesDir() + "iOS/Basic"; + return exampleRoot + "/" + name; +} + +class FlagTestSuite { +public: + FlagTestSuite() { + testBasicFlags(); + testDepFlags(); + } + + std::vector + getExampleCompileCommandForFile(std::string testFile) { + auto compDB = SetupTemplateCompilationDB(); + std::cout << testFile; + std::vector command; + for (auto item : compDB) { + auto fileName = item.second.get("file"); + auto commandStr = item.second.get("command"); + if (fileName == testFile) { + command = shlex(commandStr); + } + } + assert(command.size() > 0); + return command; + } + + void testBasicFlags() { + auto testFile = BasicExampleNamed("Basic/AppDelegate.swift"); + auto command = getExampleCompileCommandForFile(testFile); + auto prepped = FlagsForCompileCommand(command); + assert(prepped.at(0) == "-primary-file"); + assert(prepped.at(1) == testFile); + } + + void testDepFlags() { + auto testFile = BasicExampleNamed("Basic/ViewController.swift"); + auto depFile = BasicExampleNamed("Basic/AppDelegate.swift"); + auto command = getExampleCompileCommandForFile(testFile); + auto prepped = FlagsForCompileCommand(command); + assert(prepped.at(0) == "-primary-file"); + assert(prepped.at(1) == testFile); + assert(prepped.at(2) == depFile); + } +}; + +using namespace ssvim; +static ssvim::Logger logger(LogLevelInfo); + +struct Runner { + std::string complete(std::string fileName, std::string fileContents, + std::vector flags, unsigned line, + unsigned column) { + auto completer = SwiftCompleter(LogLevelExtreme); + auto files = std::vector(); + auto unsavedFile = UnsavedFile(); + unsavedFile.contents = fileContents; + unsavedFile.fileName = fileName; + + files.push_back(unsavedFile); + auto result = completer.CandidatesForLocationInFile(fileName, line, column, + files, flags); + + return result; + } +}; + +class CompleterSmokeTestSuite { +public: + CompleterSmokeTestSuite() { + logger << "Running Smoke tests"; + Runner runner; + vector flags; + flags.push_back("-sdk"); + flags.push_back("/Applications/Xcode.app/Contents/Developer/Platforms/" + "MacOSX.platform/Developer/SDKs/MacOSX.sdk"); + flags.push_back("-target"); + flags.push_back("x86_64-apple-macosx10.12"); + + auto exampleDir = GetExamplesDir(); + auto exampleName = exampleDir + std::string("some_swift.swift"); + auto example = ReadFile(exampleName); + std::cout << example; + auto result = runner.complete(exampleName, example, flags, 19, 16); + + // FIXME: parse this response. + // we should have a massive response here. + assert(result.size() > 500); + logger << "Ran Smoke tests"; + } +}; + +int wrapped_main() { + FlagTestSuite suite; + // TODO: this should be probably moved out of here. We are doing + // assertions on code paths that we don't own + CompleterSmokeTestSuite smokeTest; + exit(0); +} + +int main() { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ + wrapped_main(); + }); + dispatch_main(); +}