Commit a1dbc948 authored by Fabio Pelosin's avatar Fabio Pelosin

[Linter] Adoped Core Linter and renamed to DeepLinter (WIP).

parent 8e74ed69
...@@ -8,7 +8,7 @@ module Pod ...@@ -8,7 +8,7 @@ module Pod
class Command < CLAide::Command class Command < CLAide::Command
autoload :ErrorReport, 'cocoapods/command/error_report' autoload :ErrorReport, 'cocoapods/command/error_report'
autoload :Linter, 'cocoapods/command/linter' autoload :DeepLinter, 'cocoapods/command/deep_linter'
self.abstract_command = true self.abstract_command = true
self.command = 'pod' self.command = 'pod'
......
module Pod
class Command
class DeepLinter < Specification::Linter
include Config::Mixin
# Lints the specification adding a {Result} for any failed check to the
# {#results} list.
#
# @return [Bool] whether the specification passed validation.
#
def lint
super
unless quick
check_repo_path
spec.available_platforms.each do |platform|
UI.section "\n\n#{spec} - Analyzing on #{platform} platform.".green.reversed do
current_platform = platform
set_up_lint_environment
install_pod
build_pod
check_file_patterns
tear_down_lint_environment
end
end
end
errors.empty? && warnings.empty? && deprecations.empty?
end
# @return [Bool] whether the lint should skip the checks that requires
# the download or the build of the library.
#
attr_accessor :quick
# @return [Bool] whether the linter should not clean up temporary files
# for inspection.
#
attr_accessor :no_clean
# @return [Pathname] whether the lint should be performed
#
attr_accessor :repo_path
# @return [Bool] whether the lint should be performed against the root of
# the podspec instead to its original source. Uses the `:local` option
# of the Podfile.
#
attr_writer :local
def local?; @local; end
#-----------------------------------------------------------------------#
# !@group Lint results
public
# @return [Array<Result>] all the notes generated by the Linter.
#
def notes
@errors ||= results.select { |r| r.type == :note }
end
#-----------------------------------------------------------------------#
private
# !@group Lint steps
def check_repo_path
return unless repo_path
expected_path = "#{spec.name}/#{spec.version}/#{spec.name}.podspec"
path = file.relative_path_from(repo_path).to_s
unless path.end_with?(expected_path)
error "Incorrect path, the path is `#{file}` and should be `#{expected_path}`"
end
end
def set_up_lint_environment
tmp_dir.rmtree if tmp_dir.exist?
tmp_dir.mkpath
@original_config = Config.instance.clone
config.project_root = tmp_dir
config.project_pods_root = tmp_dir + 'Pods'
config.silent = !config.verbose
config.integrate_targets = false
config.generate_docs = false
end
def tear_down_lint_environment
tmp_dir.rmtree unless no_clean
Config.instance = @original_config
end
# It creates a podfile in memory and builds a library containing
# the pod for all available platforms with xcodebuild.
#
def install_pod
spec.activate_platform(current_platform)
podfile = podfile_from_spec
config.verbose
config.skip_repo_update = true
sandbox = Sandbox.new(config.project_pods_root)
resolver = Resolver.new(podfile, nil, sandbox)
installer = Installer.new(resolver)
installer.install!
@pod = installer.pods.find { |pod| pod.top_specification == spec }
config.silent
end
# Performs platform specific analysis.
# It requires to download the source at each iteration
#
# @note Treat xcodebuild warnings as notes because the spec maintainer
# might not be the author of the library
#
def build_pod
if `which xcodebuild`.strip.empty?
UI.warn "Skipping compilation with `xcodebuild' because it can't be found.\n".yellow
else
UI.message "\nBuilding with xcodebuild.\n".yellow if config.verbose? do
messages = []
output = Dir.chdir(config.project_pods_root) { `xcodebuild clean build 2>&1` }
clean_output = parse_xcodebuild_output(output)
messages += clean_output
puts(output) if config.verbose?
messages.each { |msg| ( msg.include?('error: ') ? @platform_errors[@platform] : @platform_notes[@platform] ) << msg }
end
end
end
# It checks that every file pattern specified in a spec yields
# at least one file. It requires the pods to be already present
# in the current working directory under Pods/spec.name.
#
# @return [Array<String>]
#
def check_file_patterns
error "The sources did not match any file" if !spec.source_files.empty? && @pod.source_files.empty?
error "The resources did not match any file" if !spec.resources.empty? && @pod.resource_files.empty?
error "The preserve_paths did not match any file" if !spec.preserve_paths.empty? && @pod.preserve_files.empty?
error "The exclude_header_search_paths did not match any file" if !spec.exclude_header_search_paths.empty? && @pod.headers_excluded_from_search_paths.empty?
unless @pod.license_file || spec.license && ( spec.license[:type] == 'Public Domain' || spec.license[:text] )
warning "Unable to find a license file"
end
end
# @errors += (@platform_errors[platform] - @errors).map {|m| "[#{platform}] #{m}"}
#-----------------------------------------------------------------------#
private
# !@group Helpers
# @return [Podfile] a podfile that requires the specification on the
# current platform.
#
# @note The generated podfile takes into account whether the linter is
# in local mode.
#
def podfile_from_spec
name = spec.name
podspec = file.realpath
platform = current_platform
local = local?
podfile = Pod::Podfile.new do
platform(platform.to_sym, platform.deployment_target)
if (local)
pod name, :local => podspec.dirname.to_s
else
pod name, :podspec => podspec.to_s
end
end
podfile
end
# @return [Pathname] the temporary directory used by the linter.
#
def tmp_dir
Pathname.new('/tmp/CocoaPods/Lint')
end
# @return [Pathname] the root of the installed pod.
#
def pod_dir
tmp_dir + 'Pods' + spec.name
end
# Parse the xcode build output to identify the lines which are relevant
# to the linter. It also removes the indentation and the temporary path.
#
# @param [String] output
# the output generated by the xcodebuild tool.
#
# @return [Array<String>] the lines that are relevant to the linter.
#
def parse_xcodebuild_output(output)
lines = output.split("\n")
selected_lines = lines.select do |l|
l.include?('error: ') &&
(l !~ /errors? generated\./) && (l !~ /error: \(null\)/) ||
l.include?('warning: ') && (l !~ /warnings? generated\./) ||
l.include?('note: ') && (l !~ /expanded from macro/)
end
selected_lines.map do |l|
new = l.gsub(/\/tmp\/CocoaPods\/Lint\/Pods\//,'') # Remove the unnecessary tmp path
new.gsub!(/^ */,' ') # Remove indentation
"XCODEBUILD > " << new # Mark
end
end
end
end
end
module Pod
class Command
class Linter
include Config::Mixin
# TODO: Add check to ensure that attributes inherited by subspecs are not duplicated ?
attr_accessor :quick, :no_clean, :repo_path
# @return [Bool] Wether the lint should be performed against the root of
# the podspec instead to its original source. Uses the `:local` option
# of the Podfile.
#
attr_accessor :local
alias :local? :local
attr_reader :spec, :file
attr_reader :errors, :warnings, :notes
def initialize(podspec)
@file = podspec
end
def spec_name
name = file.basename('.*').to_s
if @spec
name << " (#{spec.version})"
elsif @repo_path
name << " (#{file.dirname.basename})"
end
name
end
# Takes an array of podspec files and lints them all
#
# It returns true if the spec passed validation
#
def lint
@errors, @warnings, @notes = [], [], []
@platform_errors, @platform_warnings, @platform_notes = {}, {}, {}
if !deprecation_errors.empty?
@errors = deprecation_errors
@errors << "#{spec_name} [!] Fatal errors found skipping the rest of the validation"
else
@spec = Specification.from_file(file)
platforms = spec.available_platforms
if @repo_path
expected_path = "#{@spec.name}/#{@spec.version}/#{@spec.name}.podspec"
path = file.relative_path_from(@repo_path).to_s
@errors << "Incorrect path, the path is `#{file}` and should be `#{expected_path}`" unless path.end_with?(expected_path)
end
platforms.each do |platform|
@platform_errors[platform], @platform_warnings[platform], @platform_notes[platform] = [], [], []
spec.activate_platform(platform)
@platform = platform
puts "\n\n#{spec} - Analyzing on #{platform} platform.".green.reversed if config.verbose? && !@quick
# Skip validation if there are errors in the podspec as it would result in a crash
if !podspec_errors.empty?
@platform_errors[platform] += podspec_errors
@platform_notes[platform] << "#{platform.name} [!] Fatal errors found skipping the rest of the validation"
else
@platform_warnings[platform] += podspec_warnings
peform_extensive_analysis unless quick
end
end
# Get common messages
@errors += @platform_errors.values.reduce(:&)
@warnings += @platform_warnings.values.reduce(:&)
@notes += @platform_notes.values.reduce(:&)
platforms.each do |platform|
# Mark platform specific messages
@errors += (@platform_errors[platform] - @errors).map {|m| "[#{platform}] #{m}"}
@warnings += (@platform_warnings[platform] - @warnings).map {|m| "[#{platform}] #{m}"}
@notes += (@platform_notes[platform] - @notes).map {|m| "[#{platform}] #{m}"}
end
end
end
def result_type
return :error unless errors.empty?
return :warning unless warnings.empty?
return :note unless notes.empty?
:success
end
# Performs platform specific analysis.
# It requires to download the source at each iteration
#
def peform_extensive_analysis
set_up_lint_environment
install_pod
if `which xcodebuild`.strip.empty?
puts "Skipping compilation with `xcodebuild' because it can't be found.\n".yellow if config.verbose?
else
puts "\nBuilding with xcodebuild.\n".yellow if config.verbose?
# treat xcodebuild warnings as notes because the spec maintainer might not be the author of the library
xcodebuild_output.each { |msg| ( msg.include?('error: ') ? @platform_errors[@platform] : @platform_notes[@platform] ) << msg }
end
@platform_errors[@platform] += file_patterns_errors
@platform_warnings[@platform] += file_patterns_warnings
tear_down_lint_environment
end
def install_pod
podfile = podfile_from_spec
config.verbose
config.skip_repo_update = true
sandbox = Sandbox.new(config.project_pods_root)
resolver = Resolver.new(podfile, nil, sandbox)
installer = Installer.new(resolver)
installer.install!
@pod = installer.pods.find { |pod| pod.top_specification == spec }
config.silent
end
def podfile_from_spec
name = spec.name
podspec = file.realpath
platform = @platform
local = local?
podfile = Pod::Podfile.new do
platform(platform.to_sym, platform.deployment_target)
if (local)
pod name, :local => podspec.dirname.to_s
else
pod name, :podspec => podspec.to_s
end
end
podfile
end
def set_up_lint_environment
tmp_dir.rmtree if tmp_dir.exist?
tmp_dir.mkpath
@original_config = Config.instance.clone
config.project_root = tmp_dir
config.project_pods_root = tmp_dir + 'Pods'
config.silent = !config.verbose
config.integrate_targets = false
config.generate_docs = false
end
def tear_down_lint_environment
tmp_dir.rmtree unless no_clean
Config.instance = @original_config
end
def tmp_dir
Pathname.new('/tmp/CocoaPods/Lint')
end
def pod_dir
tmp_dir + 'Pods' + spec.name
end
# It reads a podspec file and checks for strings corresponding
# to features that are or will be deprecated
#
# @return [Array<String>]
#
def deprecation_errors
text = @file.read
deprecations = []
deprecations << "`config.ios?' and `config.osx?' are deprecated" if text. =~ /config\..?os.?/
deprecations << "clean_paths are deprecated and ignored (use preserve_paths)" if text. =~ /clean_paths/
deprecations
end
# @return [Array<String>] List of the fatal defects detected in a podspec
def podspec_errors
messages = []
messages << "The name of the spec should match the name of the file" unless names_match?
messages << "Unrecognized platfrom" unless platform_valid?
messages << "Missing name" unless spec.name
messages << "Missing version" unless spec.version
messages << "Missing summary" if !spec.summary || spec.summary.empty?
messages << "Missing homepage" unless spec.homepage
messages << "Missing author(s)" unless spec.authors
messages << "Missing or invalid source: #{spec.source}" unless source_valid?
messages << "The summary should be short use `description` (max 140 characters)." if spec.summary && spec.summary.length > 140
# attributes with multiplatform values
return messages unless platform_valid?
messages << "The spec appears to be empty (no source files, resources, or preserve paths)" if spec.source_files.empty? && spec.subspecs.empty? && spec.resources.empty? && spec.preserve_paths.empty?
messages += paths_starting_with_a_slash_errors
messages += deprecation_errors
messages
end
def names_match?
return true unless spec.name
root_name = spec.name.match(/[^\/]*/)[0]
file.basename.to_s == root_name + '.podspec'
end
def platform_valid?
!spec.platform || [:ios, :osx].include?(spec.platform.name)
end
def source_valid?
spec.source && !(spec.source =~ /http:\/\/EXAMPLE/)
end
def paths_starting_with_a_slash_errors
messages = []
%w[source_files public_header_files resources clean_paths].each do |accessor|
patterns = spec.send(accessor.to_sym)
# Some values are multiplaform
patterns = patterns.is_a?(Hash) ? patterns.values.flatten(1) : patterns
patterns = patterns.compact # some patterns may be nil (public_header_files, for instance)
patterns.each do |pattern|
# Skip FileList that would otherwise be resolved from the working directory resulting
# in a potentially very expensi operation
next if pattern.is_a?(FileList)
invalid = pattern.is_a?(Array) ? pattern.any? { |path| path.start_with?('/') } : pattern.start_with?('/')
if invalid
messages << "Paths cannot start with a slash (#{accessor})"
break
end
end
end
messages
end
# @return [Array<String>] List of the **non** fatal defects detected in a podspec
def podspec_warnings
license = spec.license || {}
source = spec.source || {}
text = @file.read
messages = []
messages << "Missing license type" unless license[:type]
messages << "Sample license type" if license[:type] && license[:type] =~ /\(example\)/
messages << "Invalid license type" if license[:type] && license[:type] =~ /\n/
messages << "The summary is not meaningful" if spec.summary =~ /A short description of/
messages << "The description is not meaningful" if spec.description && spec.description =~ /An optional longer description of/
messages << "The summary should end with a dot" if spec.summary !~ /.*\./
messages << "The description should end with a dot" if spec.description !~ /.*\./ && spec.description != spec.summary
messages << "The summary should end with a dot" if spec.summary !~ /.*\./
messages << "Comments must be deleted" if text.scan(/^\s*#/).length > 24
messages << "Warnings must not be disabled (`-Wno' compiler flags)" if spec.compiler_flags.split(' ').any? {|flag| flag.start_with?('-Wno') }
if (git_source = source[:git])
messages << "Git sources should specify either a tag or a commit" unless source[:commit] || source[:tag]
if spec.version.to_s != '0.0.1'
messages << "The version of the spec should be part of the git tag (not always applicable)" if source[:tag] && !source[:tag].include?(spec.version.to_s)
messages << "Git sources without tag should be marked as 0.0.1 (not always applicable)" if !source[:tag]
end
if git_source.include?('github.com')
messages << "Github repositories should end in `.git'" unless git_source.end_with?('.git')
messages << "Github repositories should use `https' link" unless git_source.start_with?('https://github.com') || git_source.start_with?('git://gist.github.com')
end
end
messages
end
# It creates a podfile in memory and builds a library containing
# the pod for all available platfroms with xcodebuild.
#
# @return [Array<String>]
#
def xcodebuild_output
return [] if `which xcodebuild`.strip.empty?
messages = []
output = Dir.chdir(config.project_pods_root) { `xcodebuild clean build 2>&1` }
clean_output = process_xcode_build_output(output)
messages += clean_output
puts(output) if config.verbose?
messages
end
def process_xcode_build_output(output)
output_by_line = output.split("\n")
selected_lines = output_by_line.select do |l|
l.include?('error: ') && (l !~ /errors? generated\./) && (l !~ /error: \(null\)/)\
|| l.include?('warning: ') && (l !~ /warnings? generated\./)\
|| l.include?('note: ') && (l !~ /expanded from macro/)
end
selected_lines.map do |l|
new = l.gsub(/\/tmp\/CocoaPods\/Lint\/Pods\//,'') # Remove the unnecessary tmp path
new.gsub!(/^ */,' ') # Remove indentation
"XCODEBUILD > " << new # Mark
end
end
# It checks that every file pattern specified in a spec yields
# at least one file. It requires the pods to be alredy present
# in the current working directory under Pods/spec.name.
#
# @return [Array<String>]
#
def file_patterns_errors
messages = []
messages << "The sources did not match any file" if !spec.source_files.empty? && @pod.source_files.empty?
messages << "The resources did not match any file" if !spec.resources.empty? && @pod.resource_files.empty?
messages << "The preserve_paths did not match any file" if !spec.preserve_paths.empty? && @pod.preserve_files.empty?
messages << "The exclude_header_search_paths did not match any file" if !spec.exclude_header_search_paths.empty? && @pod.headers_excluded_from_search_paths.empty?
messages
end
def file_patterns_warnings
messages = []
unless @pod.license_file || spec.license && ( spec.license[:type] == 'Public Domain' || spec.license[:text] )
messages << "Unable to find a license file"
end
messages
end
end
end
end
require File.expand_path('../../../spec_helper', __FILE__)
module Pod
describe Command::DeepLinter do
extend SpecHelper::TemporaryDirectory
def write_podspec(text, name = 'JSONKit.podspec')
file = temporary_directory + 'JSONKit.podspec'
File.open(file, 'w') {|f| f.write(text) }
file
end
def stub_podspec(pattern = nil, replacement = nil)
spec = (fixture('spec-repos') + 'master/JSONKit/1.4/JSONKit.podspec').read
spec.gsub!(/https:\/\/github\.com\/johnezang\/JSONKit\.git/, fixture('integration/JSONKit').to_s)
spec.gsub!(pattern, replacement) if pattern && replacement
spec
end
it "respects quick mode" do
file = write_podspec(stub_podspec)
linter = Command::DeepLinter.new(file)
linter.expects(:peform_multiplatform_analysis).never
linter.expects(:install_pod).never
linter.expects(:xcodebuild_output_for_platfrom).never
linter.expects(:file_patterns_errors_for_platfrom).never
linter.quick = true
linter.lint
end
unless skip_xcodebuild?
it "uses xcodebuild to generate notes and warnings" do
file = write_podspec(stub_podspec)
linter = Command::DeepLinter.new(file)
linter.lint
linter.result_type.should == :warning
linter.notes.join(' | ').should.include "JSONKit/JSONKit.m:1640:27: warning: equality comparison with extraneous parentheses"
end
end
it "checks for file patterns" do
file = write_podspec(stub_podspec(/s\.source_files = 'JSONKit\.\*'/, "s.source_files = 'JSONKit.*'\ns.resources = 'WRONG_FOLDER'"))
linter = Command::DeepLinter.new(file)
linter.stubs(:xcodebuild_output).returns([])
linter.quick = false
linter.lint
linter.result_type.should == :error
linter.errors.join(' | ').should.include "The resources did not match any file"
end
it "uses the deployment target of the specification" do
file = write_podspec(stub_podspec(/s.name *= 'JSONKit'/, "s.name = 'JSONKit'; s.platform = :ios, '5.0'"))
linter = Command::DeepLinter.new(file)
linter.quick = true
linter.lint
podfile = linter.podfile_from_spec
deployment_target = podfile.target_definitions[:default].platform.deployment_target
deployment_target.to_s.should == "5.0"
end
end
end
require File.expand_path('../../../spec_helper', __FILE__)
describe "Pod::Command::Linter" do
extend SpecHelper::TemporaryDirectory
def write_podspec(text, name = 'JSONKit.podspec')
file = temporary_directory + 'JSONKit.podspec'
File.open(file, 'w') {|f| f.write(text) }
file
end
def stub_podspec(pattern = nil, replacement = nil)
spec = (fixture('spec-repos') + 'master/JSONKit/1.4/JSONKit.podspec').read
spec.gsub!(/https:\/\/github\.com\/johnezang\/JSONKit\.git/, fixture('integration/JSONKit').to_s)
spec.gsub!(pattern, replacement) if pattern && replacement
spec
end
it "fails a specifications that does not contain the minimum required attributes" do
file = write_podspec('Pod::Spec.new do |s| end')
linter = Pod::Command::Spec::Linter.new(file)
linter.quick = true
linter.lint
linter.result_type.should == :error
linter.errors.join(' | ') =~ /name.*version.*summary.*homepage.*authors.*(source.*part_of).*source_files/
end
it "fails specifications if the name does not match the name of the file" do
file = write_podspec(stub_podspec(/s.name *= 'JSONKit'/, "s.name = 'JSONKitAAA'"))
linter = Pod::Command::Spec::Linter.new(file)
linter.quick = true
linter.lint
linter.result_type.should == :error
linter.errors.count.should == 1
linter.errors[0].should =~ /The name of the spec should match the name of the file/
end
it "fails a specification if a path starts with a slash" do
file = write_podspec(stub_podspec(/s.source_files = 'JSONKit\.\*'/, "s.source_files = '/JSONKit.*'"))
linter = Pod::Command::Spec::Linter.new(file)
linter.quick = true
linter.lint
linter.result_type.should == :error
linter.errors.count.should == 1
linter.errors[0].should =~ /Paths cannot start with a slash/
end
it "fails a specification if the platform is unrecognized" do
file = write_podspec(stub_podspec(/s.name *= 'JSONKit'/, "s.name = 'JSONKit'\ns.platform = :iososx\n"))
linter = Pod::Command::Spec::Linter.new(file)
linter.quick = true
linter.lint
linter.result_type.should == :error
linter.errors.count.should == 1
linter.errors[0].should =~ /Unrecognized platfrom/
end
it "fails validation if the specification contains warnings" do
file = write_podspec(stub_podspec(/.*license.*/, ""))
linter = Pod::Command::Spec::Linter.new(file)
linter.quick = true
linter.lint
linter.result_type.should == :warning
linter.errors.should.be.empty
linter.warnings.should.not.be.empty
end
it "correctly report specification that only contain warnings" do
file = write_podspec(stub_podspec(/.*license.*/, ""))
linter = Pod::Command::Spec::Linter.new(file)
linter.quick = true
linter.lint
linter.result_type.should == :warning
end
it "respects quick mode" do
file = write_podspec(stub_podspec)
linter = Pod::Command::Spec::Linter.new(file)
linter.expects(:peform_multiplatform_analysis).never
linter.expects(:install_pod).never
linter.expects(:xcodebuild_output_for_platfrom).never
linter.expects(:file_patterns_errors_for_platfrom).never
linter.quick = true
linter.lint
end
it "produces deprecation notices" do
file = write_podspec(stub_podspec(/s\.source_files = 'JSONKit\.\*'/, "s.source_files = 'JSONKit.*'\n if config.ios?\nend"))
linter = Pod::Command::Spec::Linter.new(file)
linter.quick = true
linter.lint
linter.result_type.should == :error
linter.warnings.should.be.empty
linter.errors.join(' | ').should =~ /`config.ios\?' and `config.osx\?' are deprecated/
end
unless skip_xcodebuild?
it "uses xcodebuild to generate notes and warnings" do
file = write_podspec(stub_podspec)
linter = Pod::Command::Spec::Linter.new(file)
linter.lint
linter.result_type.should == :warning
linter.notes.join(' | ').should.include "JSONKit/JSONKit.m:1640:27: warning: equality comparison with extraneous parentheses"
end
end
it "checks for file patterns" do
file = write_podspec(stub_podspec(/s\.source_files = 'JSONKit\.\*'/, "s.source_files = 'JSONKit.*'\ns.resources = 'WRONG_FOLDER'"))
linter = Pod::Command::Spec::Linter.new(file)
linter.stubs(:xcodebuild_output).returns([])
linter.quick = false
linter.lint
linter.result_type.should == :error
linter.errors.join(' | ').should.include "The resources did not match any file"
end
it "uses the deployment target of the specification" do
file = write_podspec(stub_podspec(/s.name *= 'JSONKit'/, "s.name = 'JSONKit'; s.platform = :ios, '5.0'"))
linter = Pod::Command::Spec::Linter.new(file)
linter.quick = true
linter.lint
podfile = linter.podfile_from_spec
deployment_target = podfile.target_definitions[:default].platform.deployment_target
deployment_target.to_s.should == "5.0"
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment