diff --git a/analyzer/Gemfile b/analyzer/Gemfile
new file mode 100644
index 00000000..cd8aa9e0
--- /dev/null
+++ b/analyzer/Gemfile
@@ -0,0 +1,3 @@
+source 'https://rubygems.org'
+
+gemspec
\ No newline at end of file
diff --git a/analyzer/README.md b/analyzer/README.md
new file mode 100644
index 00000000..dc40239a
--- /dev/null
+++ b/analyzer/README.md
@@ -0,0 +1,33 @@
+# README
+
+# Installation
+
+```sh
+gem build analyzer.gemspec --output=analyzer.gem
+```
+```sh
+gem install analyzer.gem
+```
+
+# Usage
+
+```sh
+analyzer
+```
+
+To see available options: `analyzer -h`
+
+# Requirements
+
+* Xcode
+* XCPretty
+* Slather
+* SwiftLint
+* Lizard
+* OCLint
+* FauxPas
+* SonarScanner
+
+## Behavior
+
+When a tool is not installed, the analyzer will gracefully skip it.
\ No newline at end of file
diff --git a/analyzer/analyzer.gemspec b/analyzer/analyzer.gemspec
new file mode 100644
index 00000000..704c82e5
--- /dev/null
+++ b/analyzer/analyzer.gemspec
@@ -0,0 +1,24 @@
+lib = File.expand_path('../lib', __FILE__)
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
+require 'version'
+
+Gem::Specification.new do |s|
+ s.name = "sonar-swift-analyzer"
+ s.version = AnalyzerV::VERSION
+ s.summary = "SonarSwift plugin analyzer"
+ s.authors = ["Gaël Foppolo"]
+
+ s.description = "SonarSwift plugin analyzer"
+ s.license = 'MIT'
+
+ s.required_ruby_version = '>= 2.0.0'
+
+ s.files = `git ls-files`.split($/)
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
+ s.require_paths = ["lib"]
+
+ s.add_runtime_dependency 'java-properties', "= 0.2.0"
+ s.add_runtime_dependency 'slather', "= 2.4.7"
+ s.add_runtime_dependency 'xcpretty', "= 0.3.0"
+
+end
\ No newline at end of file
diff --git a/analyzer/bin/analyzer b/analyzer/bin/analyzer
new file mode 100755
index 00000000..9ddaea74
--- /dev/null
+++ b/analyzer/bin/analyzer
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+
+require 'analyzer'
+
+Analyzer.new.run
\ No newline at end of file
diff --git a/analyzer/lib/analyzer.rb b/analyzer/lib/analyzer.rb
new file mode 100644
index 00000000..324d6831
--- /dev/null
+++ b/analyzer/lib/analyzer.rb
@@ -0,0 +1,78 @@
+#!/usr/bin/env ruby
+require 'ostruct'
+require 'logging'
+require 'tools/swiftlint'
+require 'tools/lizard'
+require 'tools/sonar_scanner'
+require 'tools/sonar_runner'
+require 'tools/unit_tests'
+require 'tools/slather'
+require 'tools/oclint'
+require 'tools/fauxpas'
+require 'tools/json_compilation_database'
+require 'options'
+require 'properties_reader'
+require 'helper'
+
+# Entry point of the script.
+#
+# It reads the configurations, run all analytic tools and send reports
+# to Sonar.
+class Analyzer
+ include Logging
+
+ def initialize
+ @options = OpenStruct.new
+ # list of tools by default
+ @options.tools = [JSONCompilationDatabase, UnitTests, Slather, SwiftLint, Lizard, OCLint, FauxPas]
+ # reporter by default
+ @options.reporter = SonarScanner
+ # upload results to SonarQube by default
+ @options.upload = true
+ # upload results to SonarQube by default
+ @options.path = 'sonar-project.properties'
+ # report folder
+ @options.report_folder = 'sonar-reports'
+
+ @helper = Helper.new
+
+ end
+
+ def run
+
+ # read CLI arguments and update configuration
+ Options.new.parse(ARGV, @options)
+
+ # Initiate reports
+ @helper.bootstrap_reports(@options.report_folder)
+
+ # Read Sonar project properties
+ @properties = SonarPropertiesReader.new(@options.path).read
+
+ tools
+ reporter if @options.upload
+
+ end
+
+ private
+ def tools
+ # Filter tools by availability
+ @options.tools = @helper.available(@options.tools)
+
+ # Run tools
+ @options.tools.each do |tool|
+ tool.new(@properties, @options).run
+ end
+ end
+
+ private
+ def reporter
+ # Filter reporters by availability
+ @options.reporter = @helper.available(@options.reporter)
+
+ # Send reports
+ @options.reporter.new(@properties, @options).run unless @options.reporter.nil?
+
+ end
+
+end
\ No newline at end of file
diff --git a/analyzer/lib/canfail.rb b/analyzer/lib/canfail.rb
new file mode 100644
index 00000000..8893d28c
--- /dev/null
+++ b/analyzer/lib/canfail.rb
@@ -0,0 +1,8 @@
+# Adds a fatal_error method that logs and exit.
+# This module expects the Logging module included.
+module CanFail
+ def fatal_error(msg)
+ logger.error(msg)
+ exit(false)
+ end
+end
diff --git a/analyzer/lib/helper.rb b/analyzer/lib/helper.rb
new file mode 100644
index 00000000..ba29f790
--- /dev/null
+++ b/analyzer/lib/helper.rb
@@ -0,0 +1,58 @@
+require 'fileutils'
+require 'logging'
+
+class Helper
+ include Logging
+
+ # Put default xml files with no tests and no coverage. This is needed to
+ # ensure a file is present, either the Xcode build test step worked or
+ # not. Without this, Sonar Scanner will fail uploading results.
+ def bootstrap_reports(folder)
+ reports_folder(folder)
+ mandatory_reports(folder)
+ end
+
+ # Check each program is available and return an updated list.
+ def available(programs)
+ case programs
+ when Array
+ programs.select do |program|
+ _available(program)
+ end
+ else
+ if _available(programs) then programs else nil end
+
+ end
+ end
+
+ # Check program is available and return an updated list.
+ private
+ def _available(program)
+ availability = program.command.values.reduce(true) { |available, tool|
+ toolAvailable = system("which #{tool} 2>&1 > /dev/null")
+ logger.warn("#{tool} is not found in PATH") if !toolAvailable
+ available &= toolAvailable
+ }
+ logger.warn("Skipping #{program}.") if !availability
+ availability
+ end
+
+ private
+ def mandatory_reports(folder)
+ logger.info('Creating default reports')
+ empty_test_report = ""
+ File.write("#{folder}/TEST-report.xml", empty_test_report)
+
+ empty_coverage_report = ""
+ File.write("#{folder}/coverage.xml", empty_coverage_report)
+ end
+
+ private
+ def reports_folder(folder)
+ logger.info("Deleting and creating directory #{folder}")
+ FileUtils.rm_rf(folder)
+ Dir.mkdir(folder)
+ end
+
+end
+
diff --git a/analyzer/lib/logging.rb b/analyzer/lib/logging.rb
new file mode 100644
index 00000000..82486339
--- /dev/null
+++ b/analyzer/lib/logging.rb
@@ -0,0 +1,29 @@
+require 'logger'
+
+# Adds logging capability where included.
+module Logging
+ def logger
+ @logger ||= Logging.logger_for(self.class.name)
+ end
+
+ # Use a hash class-ivar to cache a unique Logger per class:
+ @@loggers = {}
+ @@logger_level = Logger::INFO
+
+ class << self
+ def logger_for(classname)
+ @@loggers[classname] ||= configure_logger_for(classname)
+ end
+
+ def configure_logger_for(classname)
+ logger = Logger.new(STDOUT)
+ logger.progname = classname
+ logger.level = @@logger_level
+ logger
+ end
+
+ def logger_level=(level)
+ @@logger_level = level
+ end
+ end
+end
\ No newline at end of file
diff --git a/analyzer/lib/options.rb b/analyzer/lib/options.rb
new file mode 100644
index 00000000..f1d25e1f
--- /dev/null
+++ b/analyzer/lib/options.rb
@@ -0,0 +1,82 @@
+require 'optparse'
+
+class Options
+
+ REPORTER_ALIASES = { :scanner => "SonarScanner", :runner => "SonarRunner" }
+
+ #
+ # Return a structure describing the options.
+ #
+ def parse(args, options)
+ # The options specified on the command line will be updated in *options*.
+
+ opt_parser = OptionParser.new do |opts|
+ opts.banner = "Usage: main.rb [options]"
+
+ opts.separator ""
+ opts.separator "Specific options:"
+
+ # Optional
+ opts.on("-p", "--path PATH", String, "Path to Sonar properties file.") do |p|
+ options.path = p
+ end
+
+ # Optional
+ opts.on("-r", "--reporter [REPORTER]", REPORTER_ALIASES.keys, "Select Sonar reporter (scanner, runner)") do |r|
+ options.reporter = REPORTER_ALIASES[r]
+ end
+
+ opts.separator ""
+ opts.separator "Disable tools:"
+
+ # Optional
+ opts.on("--disable-swiftlint", "Disable SwiftLint") do |_|
+ options.tools.delete_at(options.tools.index(SwiftLint))
+ end
+
+ # Optional
+ opts.on("--disable-oclint", "Disable OCLint") do |_|
+ options.tools.delete_at(options.tools.index(JSONCompilationDatabase))
+ options.tools.delete_at(options.tools.index(OCLint))
+ end
+
+ # Optional
+ opts.on("--disable-slather", "Disable Slather") do |_|
+ options.tools.delete_at(options.tools.index(Slather))
+ end
+
+ # Optional
+ opts.on("--disable-lizard", "Disable Lizard") do |_|
+ options.tools.delete_at(options.tools.index(Lizard))
+ end
+
+ # Optional
+ opts.on("--disable-fauxpas", "Disable FauxPas") do |_|
+ options.tools.delete_at(options.tools.index(FauxPas))
+ end
+
+ # Optional
+ opts.on("--disable-upload", "Disable upload to SonarQube server") do |_|
+ options.upload = false
+ end
+
+ opts.separator ""
+ opts.separator "Common options:"
+
+ opts.on("-v", "--verbose", "Run verbosely") do |_|
+ Logging.logger_level = Logger::DEBUG
+ end
+
+ # No argument, shows at tail. This will print an options summary.
+ # Try it and see!
+ opts.on_tail("-h", "--help", "Show this message") do
+ puts opts
+ exit
+ end
+ end
+
+ opt_parser.parse!(args)
+
+ end
+
+end
\ No newline at end of file
diff --git a/analyzer/lib/properties_reader.rb b/analyzer/lib/properties_reader.rb
new file mode 100644
index 00000000..570e9d22
--- /dev/null
+++ b/analyzer/lib/properties_reader.rb
@@ -0,0 +1,68 @@
+require 'logging'
+require 'canfail'
+require 'fileutils'
+require 'java-properties'
+
+# SonarPropertiesReader reads and check the sonar-project.properties file.
+# It also creates a Hash with all read properties.
+class SonarPropertiesReader
+ include Logging
+ include CanFail
+
+ def initialize(path)
+ fatal_error("No #{path} found") unless File.exist?(path)
+ @file = path
+ end
+
+ # Read the Java properties file and return a Hash suitable for the script.
+ def read
+ logger.info('Reading Sonar project properties...')
+ properties = JavaProperties.load(@file)
+ options = read_properties(properties)
+ validate_settings!(options)
+ end
+
+ private
+
+ # Map the Java properties hash to more Ruby hash
+ def read_properties(properties)
+ options = {}
+ options[:project] = properties[:'sonar.swift.project']
+ options[:workspace] = properties[:'sonar.swift.workspace']
+ options[:sources] = properties[:'sonar.sources']
+ options[:sources] = options[:sources].split(',') unless options[:sources].nil?
+ options[:scheme] = properties[:'sonar.swift.appScheme']
+ options[:configuration] = properties[:'sonar.swift.appConfiguration']
+ options[:simulator] = properties[:'sonar.swift.simulator']
+ options[:exclude_from_coverage] = properties[:'sonar.swift.excludedPathsFromCoverage']
+ options[:exclude_from_coverage] = options[:exclude_from_coverage].split(',') unless options[:exclude_from_coverage].nil?
+ options[:binary_names] = properties[:'sonar.coverage.binaryNames']
+ options[:binary_names] = options[:binary_names].split(',') unless options[:binary_names].nil?
+ options[:skip_tests] = properties[:'sonar.swift.skipTests']
+ options[:skip_tests] = options[:skip_tests].split(',') unless options[:skip_tests].nil?
+ options
+ end
+
+ def validate_settings!(options)
+ fatal_error("No project or workspace specified in #{@file}") if (options[:workspace].nil? && options[:project].nil?)
+ check_file(options[:workspace])
+ check_file(options[:project])
+ fatal_error("No sources folder specified in #{@file}") if options[:sources].nil?
+ options[:sources].each do |source|
+ check_file(source)
+ end
+ fatal_error("No scheme specified in #{@file}") if options[:scheme].nil?
+ if options[:configuration].nil?
+ logger.warn('No build configuration set, defaulting to Debug')
+ options[:configuration] = 'Debug'
+ end
+ options
+ end
+
+ def check_file(file)
+ unless file.nil?
+ fatal_error("#{file} not found") unless File.exist?(file)
+ end
+ end
+
+end
\ No newline at end of file
diff --git a/analyzer/lib/tools/fauxpas.rb b/analyzer/lib/tools/fauxpas.rb
new file mode 100644
index 00000000..4df0f2f9
--- /dev/null
+++ b/analyzer/lib/tools/fauxpas.rb
@@ -0,0 +1,40 @@
+require 'tools/tool'
+
+class FauxPas < Tool
+
+ @@REPORT_FILE = 'fauxpas.json'.freeze
+
+ def self.command
+ {
+ fauxpas: 'fauxpas'
+ }
+ end
+
+ def initialize(properties, options)
+ @workspace = properties[:workspace]
+ @project = properties[:project]
+ @scheme = properties[:scheme]
+ @report_folder = options.report_folder
+ super(properties, options)
+ end
+
+ def run()
+ logger.info('Running...')
+ cmd = "#{self.class.command[:fauxpas]} check -o json #{@project}"
+ cmd += " --workspace #{@workspace}" unless @workspace.nil?
+ cmd += " --scheme #{@scheme}"
+ cmd += " -v "
+ cmd += if logger.level == Logger::DEBUG then "yes" else "no" end
+ cmd += " > #{@report_folder}/#{@@REPORT_FILE}"
+ logger.debug("Will run `#{cmd}`")
+ system(cmd)
+ end
+
+ private
+
+ def validate_settings!
+ # @workspace is optional
+ fatal_error('A project must be set in order run FauxPas') if @project.nil?
+ fatal_error('A scheme must be set in order run FauxPas') if @scheme.nil?
+ end
+end
\ No newline at end of file
diff --git a/analyzer/lib/tools/json_compilation_database.rb b/analyzer/lib/tools/json_compilation_database.rb
new file mode 100644
index 00000000..e1b20987
--- /dev/null
+++ b/analyzer/lib/tools/json_compilation_database.rb
@@ -0,0 +1,46 @@
+require 'tools/tool'
+
+class JSONCompilationDatabase < Tool
+
+ @@TIMEOUT = '360'.freeze
+ @@COMPILE_COMMANDS_FILE = 'compile_commands.json'.freeze
+ @@XCODEBUILD_FILE = 'xcodebuild.log'.freeze
+
+ def self.command
+ {
+ xcodebuild: 'xcodebuild',
+ xcpretty: 'xcpretty'
+ }
+ end
+
+ def initialize(properties, options)
+ @workspace = properties[:workspace]
+ @project = properties[:project]
+ @scheme = properties[:scheme]
+ @simulator = properties[:simulator]
+ super(properties, options)
+ end
+
+ def run
+ logger.info('Running ...')
+ cmd = "#{self.class.command[:xcodebuild]} clean build"
+ cmd += " -workspace \"#{@workspace}\"" unless @workspace.nil?
+ cmd += " -project \"#{@project}\"" unless !@workspace.nil?
+ cmd += " -scheme \"#{@scheme}\""
+ cmd += " -destination '#{@simulator}' -destination-timeout #{@@TIMEOUT} COMPILER_INDEX_STORE_ENABLE=NO" unless @simulator.nil?
+ cmd += " | tee #{@@XCODEBUILD_FILE}"
+ cmd += " | #{self.class.command[:xcpretty]} -r json-compilation-database -o #{@@COMPILE_COMMANDS_FILE}"
+ logger.debug("Will run `#{cmd}`")
+ system(cmd)
+ end
+
+ private
+
+ def validate_settings!
+ # @workspace is optional
+ fatal_error('A project must be set in order to compute JSON compilation database') if @project.nil?
+ fatal_error('A scheme must be set in order to compute JSON compilation database') if @scheme.nil?
+ logger.warn('No simulator specified') if @simulator.nil?
+ end
+
+end
\ No newline at end of file
diff --git a/analyzer/lib/tools/lizard.rb b/analyzer/lib/tools/lizard.rb
new file mode 100644
index 00000000..a27bff96
--- /dev/null
+++ b/analyzer/lib/tools/lizard.rb
@@ -0,0 +1,38 @@
+require 'tools/tool'
+
+# Lizard computes the code complexity.
+#
+# @see http://www.lizard.ws
+class Lizard < Tool
+
+ @@REPORT_FILE = 'lizard-report.xml'.freeze
+
+ def self.command
+ {
+ lizard: 'lizard'
+ }
+ end
+
+ def initialize(properties, options)
+ @sources = properties[:sources]
+ @report_folder = options.report_folder
+ super(properties, options)
+ end
+
+ def run()
+ logger.info('Running...')
+ cmd = "#{self.class.command[:lizard]} --xml"
+ @sources.each do |source|
+ cmd += " \"#{source}\""
+ end
+ cmd += " > #{@report_folder}/#{@@REPORT_FILE}"
+ logger.debug("Will run `#{cmd}`")
+ system(cmd)
+ end
+
+ private
+
+ def validate_settings!
+ fatal_error('Sources must be set in order to compute Lizard complexity') if @sources.nil?
+ end
+end
\ No newline at end of file
diff --git a/analyzer/lib/tools/oclint.rb b/analyzer/lib/tools/oclint.rb
new file mode 100644
index 00000000..b25a0414
--- /dev/null
+++ b/analyzer/lib/tools/oclint.rb
@@ -0,0 +1,50 @@
+require 'tools/tool'
+require 'fileutils'
+
+class OCLint < Tool
+
+ @@MAX_PRIORITY = 10000
+ @@LONG_LINE_THRESHOLD = 250
+ @@REPORT_FILE = '-oclint.xml'.freeze
+
+ def self.command
+ {
+ oclint: 'oclint-json-compilation-database'
+ }
+ end
+
+ def initialize(properties, options)
+ @sources = properties[:sources]
+ @report_folder = options.report_folder
+ super(properties, options)
+ end
+
+ def run()
+ logger.info('Running...')
+ @sources.each do |source|
+
+ next unless `find \"#{source}/\" -name '*.m' | wc -l | tr -d ' ' 2>&1` != 0
+
+ report_name = "#{source.tr(' ', '_')}#{@@REPORT_FILE}"
+
+ cmd = "#{self.class.command[:oclint]}"
+ cmd += " -v" if logger.level == Logger::DEBUG
+ cmd += " --include #{source}"
+ cmd += " -- -rc LONG_LINE=#{@@LONG_LINE_THRESHOLD}"
+ cmd += " -max-priority-1 #{@@MAX_PRIORITY} -max-priority-2 #{@@MAX_PRIORITY} -max-priority-3 #{@@MAX_PRIORITY}"
+ cmd += " -report-type pmd"
+ cmd += " -o #{@report_folder}/#{report_name}"
+
+ logger.debug("Will run `#{cmd}`")
+ system(cmd)
+ end
+
+ end
+
+ private
+
+ def validate_settings!
+ fatal_error('Sources must be set in order to run OCLint') if @sources.nil?
+ fatal_error("compile_commands.json not found") unless File.exist?("compile_commands.json")
+ end
+end
\ No newline at end of file
diff --git a/analyzer/lib/tools/slather.rb b/analyzer/lib/tools/slather.rb
new file mode 100644
index 00000000..da7d76d8
--- /dev/null
+++ b/analyzer/lib/tools/slather.rb
@@ -0,0 +1,59 @@
+require 'fileutils'
+require 'tools/tool'
+
+class Slather < Tool
+
+ @@REPORT_FILE_DEFAULT = 'cobertura.xml'.freeze
+ @@REPORT_FILE = 'coverage-swift.xml'.freeze
+
+ def self.command
+ {
+ slather: 'slather'
+ }
+ end
+
+ def initialize(properties, options)
+ @workspace = properties[:workspace]
+ @project = properties[:project]
+ @scheme = properties[:scheme]
+ @exclude_from_coverage = properties[:exclude_from_coverage]
+ @binary_names = properties[:binary_names]
+ @report_folder = options.report_folder
+ super(properties, options)
+ end
+
+ def run()
+ logger.info('Running...')
+
+ cmd = "#{self.class.command[:slather]} coverage"
+ cmd += " --verbose" if logger.level == Logger::DEBUG
+ unless @binary_names.nil?
+ @binary_names.each do |binary|
+ cmd += " --binary-basename \"#{binary}\""
+ end
+ end
+ unless @exclude_from_coverage.nil?
+ @exclude_from_coverage.each do |exclusion|
+ cmd += " -i \"#{exclusion}\""
+ end
+ end
+ cmd += " --input-format profdata --cobertura-xml --output-directory #{@report_folder}"
+ cmd += " --workspace #{@workspace}" unless @workspace.nil?
+ cmd += " --scheme #{@scheme} #{@project}"
+ logger.debug("Will run `#{cmd}`")
+ system(cmd)
+
+ FileUtils.mv("#{@report_folder}/#{@@REPORT_FILE_DEFAULT}", "#{@report_folder}/#{@@REPORT_FILE}")
+
+ end
+
+ private
+
+ def validate_settings!
+ # @workspace is optional
+ fatal_error('A project must be set in order to run Slather') if @project.nil?
+ fatal_error('A scheme must be set in order to run Slather') if @scheme.nil?
+ # @exclude_from_coverage is optional
+ # @binary_names is optional
+ end
+end
\ No newline at end of file
diff --git a/analyzer/lib/tools/sonar_runner.rb b/analyzer/lib/tools/sonar_runner.rb
new file mode 100644
index 00000000..368eecbd
--- /dev/null
+++ b/analyzer/lib/tools/sonar_runner.rb
@@ -0,0 +1,21 @@
+require 'tools/tool'
+
+class SonarRunner < Tool
+ def self.command
+ {
+ runner: 'sonar-runner'
+ }
+ end
+
+ def initialize(properties, options)
+ super(properties, options)
+ end
+
+ def run()
+ logger.info('Running...')
+ cmd = "#{self.class.command[:runner]}"
+ logger.debug("Will run `#{cmd}`")
+ system(cmd)
+ end
+
+end
\ No newline at end of file
diff --git a/analyzer/lib/tools/sonar_scanner.rb b/analyzer/lib/tools/sonar_scanner.rb
new file mode 100644
index 00000000..5b586020
--- /dev/null
+++ b/analyzer/lib/tools/sonar_scanner.rb
@@ -0,0 +1,21 @@
+require 'tools/tool'
+
+class SonarScanner < Tool
+ def self.command
+ {
+ scanner: 'sonar-scanner'
+ }
+ end
+
+ def initialize(properties, options)
+ super(properties, options)
+ end
+
+ def run()
+ logger.info('Running...')
+ cmd = "#{self.class.command[:scanner]}"
+ logger.debug("Will run `#{cmd}`")
+ system(cmd)
+ end
+
+end
\ No newline at end of file
diff --git a/analyzer/lib/tools/swiftlint.rb b/analyzer/lib/tools/swiftlint.rb
new file mode 100644
index 00000000..589a712a
--- /dev/null
+++ b/analyzer/lib/tools/swiftlint.rb
@@ -0,0 +1,43 @@
+require 'tools/tool'
+
+# SwiftLint checks code style and conventions.
+#
+# It is mainly based on the [Swift Style
+# Guide](https://github.com/github/swift-style-guide) and it may also be
+# used to enforce custom conventions.
+#
+# https://github.com/realm/SwiftLint
+class SwiftLint < Tool
+
+ @@REPORT_FILE = '-swiftlint.txt'.freeze
+
+ def self.command
+ {
+ swiftlint: 'swiftlint'
+ }
+ end
+
+ def initialize(properties, options)
+ @sources = properties[:sources]
+ @report_folder = options.report_folder
+ super(properties, options)
+ end
+
+ def run()
+ logger.info('Running...')
+ @sources.each do |source|
+ report_name = "#{source.tr(' ', '_')}#{@@REPORT_FILE}"
+ cmd = "#{self.class.command[:swiftlint]} lint --path \"#{source}\""
+ cmd += " --quiet" unless logger.level == Logger::DEBUG
+ cmd += " > #{@report_folder}/#{report_name}"
+ logger.debug("Will run `#{cmd}`")
+ system(cmd)
+ end
+ end
+
+ private
+
+ def validate_settings!
+ fatal_error('Sources must be set in order to lint') if @sources.nil?
+ end
+end
\ No newline at end of file
diff --git a/analyzer/lib/tools/tool.rb b/analyzer/lib/tools/tool.rb
new file mode 100644
index 00000000..19b5c8c5
--- /dev/null
+++ b/analyzer/lib/tools/tool.rb
@@ -0,0 +1,25 @@
+require 'logging'
+require 'canfail'
+
+# A base class for tool wrappers.
+#
+# Mainly defines a common interface + includes some useful modules.
+class Tool
+ include Logging
+ include CanFail
+
+ def self.command
+ end
+
+ def initialize(properties, options)
+ validate_settings!
+ end
+
+ def run
+ end
+
+ protected
+
+ def validate_settings!
+ end
+end
\ No newline at end of file
diff --git a/analyzer/lib/tools/unit_tests.rb b/analyzer/lib/tools/unit_tests.rb
new file mode 100644
index 00000000..dc002a2f
--- /dev/null
+++ b/analyzer/lib/tools/unit_tests.rb
@@ -0,0 +1,61 @@
+require 'tools/tool'
+
+# Runs unit tests using Xcode with `xcodebuild`
+class UnitTests < Tool
+
+ @@TIMEOUT = '60'.freeze
+ @@REPORT_FILE = 'TEST-report.xml'
+
+ def self.command
+ {
+ xcodebuild: 'xcodebuild',
+ xcpretty: 'xcpretty'
+ }
+ end
+
+ def initialize(properties, options)
+ @workspace = properties[:workspace]
+ @project = properties[:project]
+ @scheme = properties[:scheme]
+ @configuration = properties[:configuration]
+ @simulator = properties[:simulator]
+ @exclude_from_coverage = properties[:exclude_from_coverage]
+ @skip_tests = properties[:skip_tests]
+ @report_folder = options.report_folder
+ super(properties, options)
+ end
+
+ def run
+ logger.info('Running...')
+ cmd = "#{self.class.command[:xcodebuild]} clean build-for-testing test"
+ cmd += " -workspace \"#{@workspace}\"" unless @workspace.nil?
+ cmd += " -project \"#{@project}\"" unless !@workspace.nil?
+ cmd += " -scheme \"#{@scheme}\""
+ cmd += " -configuration \"#{@configuration}\""
+ cmd += " -enableCodeCoverage YES"
+ cmd += " -destination '#{@simulator}' -destination-timeout #{@@TIMEOUT}" unless @simulator.nil?
+ unless @skip_tests.nil?
+ @skip_tests.each do |test|
+ cmd += " -skip-testing:#{test}"
+ end
+ end
+ cmd += " -quiet" unless logger.level == Logger::DEBUG
+ cmd += " | tee xcodebuild.log"
+ cmd += " | #{self.class.command[:xcpretty]} -t --report junit -o #{@report_folder}/#{@@REPORT_FILE}"
+ logger.debug("Will run `#{cmd}`")
+ system(cmd)
+ end
+
+ private
+
+ def validate_settings!
+ # @workspace is optional
+ fatal_error('A project must be set in order to run test') if @project.nil?
+ fatal_error('A scheme must be set in order to build and test the app') if @scheme.nil?
+ fatal_error('A configuration must be set in order to build and test the app') if @configuration.nil?
+ logger.warn('No simulator specified') if @simulator.nil?
+ # @exclude_from_coverage is optional
+ # @skip_tests is optional
+ end
+
+end
\ No newline at end of file
diff --git a/analyzer/lib/version.rb b/analyzer/lib/version.rb
new file mode 100644
index 00000000..b552c37b
--- /dev/null
+++ b/analyzer/lib/version.rb
@@ -0,0 +1,3 @@
+module AnalyzerV
+ VERSION = "0.0.1"
+end
diff --git a/sonar-project.properties b/sonar-project.properties
deleted file mode 100755
index b793770f..00000000
--- a/sonar-project.properties
+++ /dev/null
@@ -1,108 +0,0 @@
-#
-# Swift SonarQube Plugin - Enables analysis of Swift and Objective-C projects into SonarQube.
-# Copyright © 2015 Backelite (${email})
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program. If not, see .
-#
-
-sonar.projectKey=prjKey
-sonar.projectName=prjName
-# Number version (can be found automatically in plist, just comment this line)
-sonar.projectVersion=1.0
-
-# Comment if you have a project with mixed ObjC / Swift
-sonar.language=swift
-
-# Project description
-sonar.projectDescription=prjDescription
-
-# Path to source directories
-sonar.sources=SourceDir
-# Path to test directories (comment if no test)
-sonar.tests=TestDir
-
-# Destination Simulator to run surefire
-# As string expected in destination argument of xcodebuild command
-# Example = sonar.swift.simulator=platform=iOS Simulator,name=iPhone 6,OS=9.2
-sonar.swift.simulator=platform=iOS Simulator,name=iPhone X,OS=latest
-
-# Xcode project configuration (.xcodeproj)
-# and use the later to specify which project(s) to include in the analysis (comma separated list)
-# Specify either xcodeproj or xcodeproj + xcworkspace
-#sonar.swift.project=MyPrj.xcodeproj
-#sonar.swift.workspace=MyWrkSpc.xcworkspace
-
-# Scheme to build your application
-sonar.swift.appScheme=MyScheme
-
-# Specify your appname when different from targeted scheme.
-# Or when slather fails with 'No product binary found'
-# You can also provide a list of framework names to analyse for coverage.
-# This will be something like "myApp" or "myApp,myFramework"
-# sonar.coverage.binaryNames=myApp,myFramework
-
-# Configuration to use for your scheme. if you do not specify that the default will be Debug
-sonar.swift.appConfiguration=MyConfiguration
-
-##########################
-# Optional configuration #
-##########################
-# Encoding of the source code
-sonar.sourceEncoding=UTF-8
-# SCM
-# sonar.scm.enabled=true
-# sonar.scm.url=scm:git:http://xxx
-
-# JUnit report generated by run-sonar.sh is stored in sonar-reports/TEST-report.xml
-# Change it only if you generate the file on your own
-# The XML files have to be prefixed by TEST- otherwise they are not processed
-# sonar.junit.reportsPath=sonar-reports/
-
-# Lizard report generated by run-sonar.sh is stored in sonar-reports/lizard-report.xml
-# Change it only if you generate the file on your own
-# sonar.swift.lizard.report=sonar-reports/lizard-report.xml
-
-# Cobertura report generated by run-sonar.sh is stored in sonar-reports/coverage-swift.xml
-# Change it only if you generate the file on your own
-# sonar.swift.coverage.reportPattern=sonar-reports/coverage-swift*.xml
-
-# OCLint report generated by run-sonar.sh is stored in sonar-reports/oclint.xml
-# Change it only if you generate the file on your own
-# sonar.swift.swiftlint.report=sonar-reports/*swiftlint.txt
-
-# Change it only if you generate the file on your own
-# sonar.swift.tailor.report=sonar-reports/*tailor.txt
-
-# Paths to exclude from coverage report (surefire, 3rd party libraries etc.)
-# sonar.swift.excludedPathsFromCoverage=pattern1,pattern2
-sonar.swift.excludedPathsFromCoverage=.*Tests.*
-
-# Ability to skip tests (such as UI Tests running long time)
-# Example = sonar.swift.skipTests=UITests
-
-##########################
-# Tailor configuration #
-##########################
-# Tailor configuration
-# -l,--max-line-length=<0-999> maximum Line length (in characters)
-# --list-files display Swift source files to be analyzed
-# --max-class-length=<0-999> maximum Class length (in lines)
-# --max-closure-length=<0-999> maximum Closure length (in lines)
-# --max-file-length=<0-999> maximum File length (in lines)
-# --max-function-length=<0-999> maximum Function length (in lines)
-# --max-name-length=<0-999> maximum Identifier name length (in characters)
-# --max-severity= maximum severity
-# --max-struct-length=<0-999> maximum Struct length (in lines)
-# --min-name-length=<1-999> minimum Identifier name length (in characters)
-sonar.swift.tailor.config=--no-color --max-line-length=100 --max-file-length=500 --max-name-length=40 --max-name-length=40 --min-name-length=4