Skip to content
Snippets Groups Projects
audit.rb 27.5 KiB
Newer Older
require 'formula'
require 'utils'
require 'extend/ENV'
require 'formula_cellar_checks'
module Homebrew
Jack Nagel's avatar
Jack Nagel committed
  def audit
    formula_count = 0
    problem_count = 0

    strict = ARGV.include? "--strict"
    if strict && ARGV.formulae.any? && MacOS.version >= :mavericks
      require "cmd/style"
      ohai "brew style #{ARGV.formulae.join " "}"
      style
    end

    ENV.activate_extensions!
    ENV.setup_build_environment

Jack Nagel's avatar
Jack Nagel committed
    ff = if ARGV.named.empty?
Max Howell's avatar
Max Howell committed
      Formula
Jack Nagel's avatar
Jack Nagel committed
    else
      ARGV.formulae
    end

    output_header = !strict
Jack Nagel's avatar
Jack Nagel committed
    ff.each do |f|
      fa = FormulaAuditor.new(f, :strict => strict)
Jack Nagel's avatar
Jack Nagel committed
      fa.audit

      unless fa.problems.empty?
        unless output_header
          puts
          ohai "audit problems"
          output_header = true
        end

Jack Nagel's avatar
Jack Nagel committed
        formula_count += 1
        problem_count += fa.problems.size
        puts "#{f.name}:", fa.problems.map { |p| " * #{p}" }, ""
Jack Nagel's avatar
Jack Nagel committed
      end
    end

    unless problem_count.zero?
      ofail "#{problem_count} problems in #{formula_count} formulae"
    end
  end
end
Jack Nagel's avatar
Jack Nagel committed
class FormulaText
  def initialize path
    @text = path.open("rb", &:read)
Jack Nagel's avatar
Jack Nagel committed
  def without_patch
    @text.split("\n__END__").first
Jack Nagel's avatar
Jack Nagel committed
  def has_DATA?
    /^[^#]*\bDATA\b/ =~ @text
Jack Nagel's avatar
Jack Nagel committed
  def has_END?
    /^__END__$/ =~ @text
Jack Nagel's avatar
Jack Nagel committed
  def has_trailing_newline?
Jack Nagel's avatar
Jack Nagel committed
end
Jack Nagel's avatar
Jack Nagel committed
class FormulaAuditor
  include FormulaCellarChecks

  attr_reader :formula, :text, :problems
Jack Nagel's avatar
Jack Nagel committed

  BUILD_TIME_DEPS = %W[
    autoconf
    automake
    boost-build
    bsdmake
    cmake
    imake
Jack Nagel's avatar
Jack Nagel committed
    libtool
    pkg-config
    scons
    smake
  FILEUTILS_METHODS = FileUtils.singleton_methods(false).join "|"

  def initialize(formula, options={})
    @formula = formula
    @strict = !!options[:strict]
Jack Nagel's avatar
Jack Nagel committed
    @problems = []
    @text = FormulaText.new(formula.path)
    @specs = %w{stable devel head}.map { |s| formula.send(s) }.compact
Jack Nagel's avatar
Jack Nagel committed
  end

  def audit_file
    unless formula.path.stat.mode == 0100644
      problem "Incorrect file permissions: chmod 644 #{formula.path}"
Jack Nagel's avatar
Jack Nagel committed
    end
    if text.has_DATA? and not text.has_END?
Jack Nagel's avatar
Jack Nagel committed
      problem "'DATA' was found, but no '__END__'"
    end
    if text.has_END? and not text.has_DATA?
Jack Nagel's avatar
Jack Nagel committed
      problem "'__END__' was found, but 'DATA' is not used"
    end
    unless text.has_trailing_newline?
Jack Nagel's avatar
Jack Nagel committed
      problem "File should end with a newline"
    end
  def audit_class
      unless formula.test_defined?
        problem "A `test do` test block should be added"
      end
    end
Jack Nagel's avatar
Jack Nagel committed

    if formula.class < GithubGistFormula
      problem "GithubGistFormula is deprecated, use Formula instead"
    end
Jack Nagel's avatar
Jack Nagel committed
  @@aliases ||= Formula.aliases
Jack Nagel's avatar
Jack Nagel committed
  def audit_deps
Jack Nagel's avatar
Jack Nagel committed
    @specs.each do |spec|
      # Check for things we don't like to depend on.
      # We allow non-Homebrew installs whenever possible.
      spec.deps.each do |dep|
        begin
          dep_f = dep.to_formula
        rescue TapFormulaUnavailableError
          # Don't complain about missing cross-tap dependencies
          next
        rescue FormulaUnavailableError
          problem "Can't find dependency #{dep.name.inspect}."
          next
        end
Jack Nagel's avatar
Jack Nagel committed

        if @@aliases.include?(dep.name)
          problem "Dependency '#{dep.name}' is an alias; use the canonical name '#{dep.to_formula.name}'."
        end
Jack Nagel's avatar
Jack Nagel committed

        dep.options.reject do |opt|
          next true if dep_f.option_defined?(opt)
          dep_f.requirements.detect do |r|
            if r.recommended?
              opt.name == "with-#{r.name}"
            elsif r.optional?
              opt.name == "without-#{r.name}"
            end
Jack Nagel's avatar
Jack Nagel committed
        end.each do |opt|
          problem "Dependency #{dep} does not define option #{opt.name.inspect}"
Jack Nagel's avatar
Jack Nagel committed
        case dep.name
        when *BUILD_TIME_DEPS
          next if dep.build? or dep.run?
          problem <<-EOS.undent
            #{dep} dependency should be
              depends_on "#{dep}" => :build
            Or if it is indeed a runtime denpendency
              depends_on "#{dep}" => :run
          EOS
        when "git"
          problem "Use `depends_on :git` instead of `depends_on 'git'`"
        when "mercurial"
          problem "Use `depends_on :hg` instead of `depends_on 'mercurial'`"
        when "ruby"
          problem "Don't use ruby as a dependency. We allow non-Homebrew ruby installations."
Jack Nagel's avatar
Jack Nagel committed
        when 'gfortran'
          problem "Use `depends_on :fortran` instead of `depends_on 'gfortran'`"
        when 'open-mpi', 'mpich2'
          problem <<-EOS.undent
            There are multiple conflicting ways to install MPI. Use an MPIDependency:
              depends_on :mpi => [<lang list>]
            Where <lang list> is a comma delimited list that can include:
              :cc, :cxx, :f77, :f90
            EOS
        end
Jack Nagel's avatar
Jack Nagel committed
      end
    end
  def audit_conflicts
    formula.conflicts.each do |c|
        Formulary.factory(c.name)
Jack Nagel's avatar
Jack Nagel committed
      rescue FormulaUnavailableError
        problem "Can't find conflicting formula #{c.name.inspect}."
  def audit_options
    formula.options.each do |o|
      next unless @strict
      if o.name !~ /with(out)?-/ && o.name != "c++11" && o.name != "universal" && o.name != "32-bit"
        problem "Options should begin with with/without. Migrate '--#{o.name}' with `deprecated_option`."
      end
    end
  end

Jack Nagel's avatar
Jack Nagel committed
  def audit_urls
    homepage = formula.homepage

    unless homepage =~ %r[^https?://]
      problem "The homepage should start with http or https (URL is #{homepage})."
    end

    # Check for http:// GitHub homepage urls, https:// is preferred.
    # Note: only check homepages that are repo pages, not *.github.com hosts
    if homepage =~ %r[^http://github\.com/]
      problem "Use https:// URLs for homepages on GitHub (URL is #{homepage})."
Jack Nagel's avatar
Jack Nagel committed
    end
Jack Nagel's avatar
Jack Nagel committed
    # Google Code homepages should end in a slash
    if homepage =~ %r[^https?://code\.google\.com/p/[^/]+[^/]$]
      problem "Google Code homepage should end with a slash (URL is #{homepage})."
Jack Nagel's avatar
Jack Nagel committed
    end
    # Automatic redirect exists, but this is another hugely common error.
    if homepage =~ %r[^http://code\.google\.com/]
      problem "Google Code homepages should be https:// links (URL is #{homepage})."
    end

    # GNU has full SSL/TLS support but no auto-redirect.
    if homepage =~ %r[^http://www\.gnu\.org/]
      problem "GNU homepages should be https:// links (URL is #{homepage})."
    end

    # Savannah has full SSL/TLS support but no auto-redirect.
    # Doesn't apply to the download links (boo), only the homepage.
    if homepage =~ %r[^http://savannah\.nongnu\.org/]
      problem "Savannah homepages should be https:// links (URL is #{homepage})."
    end

Dominyk Tiller's avatar
Dominyk Tiller committed
    if homepage =~ %r[^http://((?:trac|tools|www)\.)?ietf\.org]
      problem "ietf homepages should be https:// links (URL is #{homepage})."
    end

    if homepage =~ %r[^http://((?:www)\.)?gnupg.org/]
      problem "GnuPG homepages should be https:// links (URL is #{homepage})."
    end

    # Freedesktop is complicated to handle - It has SSL/TLS, but only on certain subdomains.
    # To enable https Freedesktop change the url from http://project.freedesktop.org/wiki to
    # https://wiki.freedesktop.org/project_name.
    # "Software" is redirected to https://wiki.freedesktop.org/www/Software/project_name
    if homepage =~ %r[^http://((?:www|nice|libopenraw|liboil|telepathy|xorg)\.)?freedesktop\.org/(?:wiki/)?]
      if homepage =~ /Software/
        problem "The url should be styled `https://wiki.freedesktop.org/www/Software/project_name`, not #{homepage})."
      else
        problem "The url should be styled `https://wiki.freedesktop.org/project_name`, not #{homepage})."
      end
    end

    if homepage =~ %r[^http://wiki\.freedesktop\.org/]
      problem "Freedesktop's Wiki subdomain should be https:// (URL is #{homepage})."
    end

    # There's an auto-redirect here, but this mistake is incredibly common too.
    if homepage =~ %r[^http://packages\.debian\.org]
      problem "Debian homepage should be https:// links (URL is #{homepage})."
    end

Dominyk Tiller's avatar
Dominyk Tiller committed
    # People will run into mixed content sometimes, but we should enforce and then add
    # exemptions as they are discovered. Treat mixed content on homepages as a bug.
    # Justify each exemptions with a code comment so we can keep track here.
    if homepage =~ %r[^http://[^/]*github\.io/]
      problem "Github Pages links should be https:// (URL is #{homepage})."
    end

    # There's an auto-redirect here, but this mistake is incredibly common too.
    # Only applies to the homepage and subdomains for now, not the FTP links.
    if homepage =~ %r[^http://((?:build|cloud|developer|download|extensions|git|glade|help|library|live|nagios|news|people|projects|rt|static|wiki|www)\.)?gnome\.org]
      problem "Gnome homepages should be https:// links (URL is #{homepage})."
    end

Jack Nagel's avatar
Jack Nagel committed
    urls = @specs.map(&:url)
Jack Nagel's avatar
Jack Nagel committed
    # Check GNU urls; doesn't apply to mirrors
    urls.grep(%r[^(?:https?|ftp)://(?!alpha).+/gnu/]) do |u|
      problem "\"ftpmirror.gnu.org\" is preferred for GNU software (url is #{u})."
Jack Nagel's avatar
Jack Nagel committed
    end
    # the rest of the checks apply to mirrors as well.
Jack Nagel's avatar
Jack Nagel committed
    urls.concat(@specs.map(&:mirrors).flatten)
    # Check a variety of SSL/TLS links that don't consistently auto-redirect
    # or are overly common errors that need to be reduced & fixed over time.
    urls.each do |p|
      # Skip the main url link, as it can't be made SSL/TLS yet.
      next if p =~ %r[/ftpmirror\.gnu\.org]

      case p
      when %r[^http://ftp\.gnu\.org/]
        problem "ftp.gnu.org urls should be https://, not http:// (url is #{p})."
      when %r[^http://code\.google\.com/]
        problem "code.google.com urls should be https://, not http (url is #{p})."
      when %r[^http://fossies\.org/]
        problem "Fossies urls should be https://, not http (url is #{p})."
      when %r[^http://mirrors\.kernel\.org/]
        problem "mirrors.kernel urls should be https://, not http (url is #{p})."
      when %r[^http://tools\.ietf\.org/]
        problem "ietf urls should be https://, not http (url is #{p})."
      end
    end

Jack Nagel's avatar
Jack Nagel committed
    # Check SourceForge urls
    urls.each do |p|
Adam Vandenberg's avatar
Adam Vandenberg committed
      # Skip if the URL looks like a SVN repo
Jack Nagel's avatar
Jack Nagel committed
      next if p =~ %r[/svnroot/]
      next if p =~ %r[svn\.sourceforge]
Jack Nagel's avatar
Jack Nagel committed
      # Is it a sourceforge http(s) URL?
      next unless p =~ %r[^https?://.*\b(sourceforge|sf)\.(com|net)]
Jack Nagel's avatar
Jack Nagel committed
      if p =~ /(\?|&)use_mirror=/
        problem "Don't use #{$1}use_mirror in SourceForge urls (url is #{p})."
Jack Nagel's avatar
Jack Nagel committed
      end
Jack Nagel's avatar
Jack Nagel committed
      if p =~ /\/download$/
        problem "Don't use /download in SourceForge urls (url is #{p})."
Jack Nagel's avatar
Jack Nagel committed
      end
      if p =~ %r[^https?://sourceforge\.]
        problem "Use http://downloads.sourceforge.net to get geolocation (url is #{p})."
      end

      if p =~ %r[^https?://prdownloads\.]
Samuel John's avatar
Samuel John committed
        problem "Don't use prdownloads in SourceForge urls (url is #{p}).\n" +
                "\tSee: http://librelist.com/browser/homebrew/2011/1/12/prdownloads-is-bad/"
Jack Nagel's avatar
Jack Nagel committed
      end
Jack Nagel's avatar
Jack Nagel committed
      if p =~ %r[^http://\w+\.dl\.]
        problem "Don't use specific dl mirrors in SourceForge urls (url is #{p})."
Jack Nagel's avatar
Jack Nagel committed
      end

      if p.start_with? "http://downloads"
        problem "Use https:// URLs for downloads from SourceForge (url is #{p})."
      end
    # Check for Google Code download urls, https:// is preferred
    urls.grep(%r[^http://.*\.googlecode\.com/files.*]) do |u|
      problem "Use https:// URLs for downloads from Google Code (url is #{u})."
    end

    # Check for new-url Google Code download urls, https:// is preferred
    urls.grep(%r[^http://code\.google\.com/]) do |u|
      problem "Use https:// URLs for downloads from code.google (url is #{u})."
    end

    # Check for git:// GitHub repo urls, https:// is preferred.
    urls.grep(%r[^git://[^/]*github\.com/]) do |u|
      problem "Use https:// URLs for accessing GitHub repositories (url is #{u})."
Jack Nagel's avatar
Jack Nagel committed
    end
    # Check for git:// Gitorious repo urls, https:// is preferred.
    urls.grep(%r[^git://[^/]*gitorious\.org/]) do |u|
      problem "Use https:// URLs for accessing Gitorious repositories (url is #{u})."
    end

    # Check for http:// GitHub repo urls, https:// is preferred.
    urls.grep(%r[^http://github\.com/.*\.git$]) do |u|
      problem "Use https:// URLs for accessing GitHub repositories (url is #{u})."
Samuel John's avatar
Samuel John committed
    end
Adam Vandenberg's avatar
Adam Vandenberg committed
    # Use new-style archive downloads
    urls.select { |u| u =~ %r[https://.*github.*/(?:tar|zip)ball/] && u !~ %r[\.git$] }.each do |u|
Adam Vandenberg's avatar
Adam Vandenberg committed
      problem "Use /archive/ URLs for GitHub tarballs (url is #{u})."
    end

    # Don't use GitHub .zip files
    urls.select { |u| u =~ %r[https://.*github.*/(archive|releases)/.*\.zip$] && u !~ %r[releases/download] }.each do |u|
      problem "Use GitHub tarballs rather than zipballs (url is #{u})."
    end
Jack Nagel's avatar
Jack Nagel committed
  def audit_specs
Dominyk Tiller's avatar
Dominyk Tiller committed
    if head_only?(formula) && formula.tap != "Homebrew/homebrew-head-only"
      problem "Head-only (no stable download)"
    end
Dominyk Tiller's avatar
Dominyk Tiller committed
    if devel_only?(formula) && formula.tap != "Homebrew/homebrew-devel-only"
      problem "Devel-only (no stable download)"
    end

    %w[Stable Devel HEAD].each do |name|
      next unless spec = formula.send(name.downcase)
      ra = ResourceAuditor.new(spec).audit
      problems.concat ra.problems.map { |problem| "#{name}: #{problem}" }
Jack Nagel's avatar
Jack Nagel committed

      spec.resources.each_value do |resource|
        ra = ResourceAuditor.new(resource).audit
        problems.concat ra.problems.map { |problem|
          "#{name} resource #{resource.name.inspect}: #{problem}"
        }
Jack Nagel's avatar
Jack Nagel committed
      end

      spec.patches.select(&:external?).each { |p| audit_patch(p) }
    if formula.stable && formula.devel
      if formula.devel.version < formula.stable.version
        problem "devel version #{formula.devel.version} is older than stable version #{formula.stable.version}"
      elsif formula.devel.version == formula.stable.version
        problem "stable and devel versions are identical"
      end
    end

    stable = formula.stable
    if stable && stable.url =~ /#{Regexp.escape("ftp.gnome.org/pub/GNOME/sources")}/i
      minor_version = stable.version.to_s[/\d\.(\d+)/, 1].to_i

      if minor_version.odd?
        problem "#{stable.version} is a development release"
      end
    end
Jack Nagel's avatar
Jack Nagel committed
  def audit_patches
    legacy_patches = Patch.normalize_legacy_patches(formula.patches).grep(LegacyPatch)
    if legacy_patches.any?
      problem "Use the patch DSL instead of defining a 'patches' method"
      legacy_patches.each { |p| audit_patch(p) }
    end
  end

  def audit_patch(patch)
    case patch.url
    when %r[raw\.github\.com], %r[gist\.github\.com/raw], %r[gist\.github\.com/.+/raw],
      %r[gist\.githubusercontent\.com/.+/raw]
      unless patch.url =~ /[a-fA-F0-9]{40}/
        problem "GitHub/Gist patches should specify a revision:\n#{patch.url}"
Jack Nagel's avatar
Jack Nagel committed
      end
    when %r[macports/trunk]
      problem "MacPorts patches should specify a revision instead of trunk:\n#{patch.url}"
    when %r[^http://trac\.macports\.org]
      problem "Patches from MacPorts Trac should be https://, not http:\n#{patch.url}"
    when %r[^http://bugs\.debian\.org]
      problem "Patches from Debian should be https://, not http:\n#{patch.url}"
    when %r[^https?://github\.com/.*commit.*\.patch$]
      problem "GitHub appends a git version to patches; use .diff instead."
Jack Nagel's avatar
Jack Nagel committed
  end
Jack Nagel's avatar
Jack Nagel committed
  def audit_text
Adam Vandenberg's avatar
Adam Vandenberg committed
    if text =~ /system\s+['"]scons/
Adam Vandenberg's avatar
Adam Vandenberg committed
      problem "use \"scons *args\" instead of \"system 'scons', *args\""
    if text =~ /system\s+['"]xcodebuild/
      problem %{use "xcodebuild *args" instead of "system 'xcodebuild', *args"}
    end

    if text =~ /xcodebuild[ (]["'*]/ && text !~ /SYMROOT=/
      problem %{xcodebuild should be passed an explicit "SYMROOT"}
Jack Nagel's avatar
Jack Nagel committed
    end

    if text =~ /Formula\.factory\(/
      problem "\"Formula.factory(name)\" is deprecated in favor of \"Formula[name]\""
    end
Jack Nagel's avatar
Jack Nagel committed
  end

  def audit_line(line, lineno)
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /<(Formula|AmazonWebServicesFormula|ScriptFileFormula|GithubGistFormula)/
Jack Nagel's avatar
Jack Nagel committed
      problem "Use a space in class inheritance: class Foo < #{$1}"
Jack Nagel's avatar
Jack Nagel committed
    # Commented-out cmake support from default template
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /# system "cmake/
      problem "Commented cmake call found"
    # Comments from default template
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /# PLEASE REMOVE/
      problem "Please remove default template comments"
    end
    if line =~ /# Documentation:/
      problem "Please remove default template comments"
    end
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /# if this fails, try separate make\/make install steps/
      problem "Please remove default template comments"
    end
    if line =~ /# The url of the archive/
      problem "Please remove default template comments"
    end
    if line =~ /## Naming --/
      problem "Please remove default template comments"
    end
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /# if your formula requires any X11\/XQuartz components/
      problem "Please remove default template comments"
    end
    if line =~ /# if your formula fails when building in parallel/
      problem "Please remove default template comments"
    end
    if line =~ /# Remove unrecognized options if warned by configure/
      problem "Please remove default template comments"
    end

Jack Nagel's avatar
Jack Nagel committed
    # FileUtils is included in Formula
Adam Vandenberg's avatar
Adam Vandenberg committed
    # encfs modifies a file with this name, so check for some leading characters
    if line =~ /[^'"\/]FileUtils\.(\w+)/
Jack Nagel's avatar
Jack Nagel committed
      problem "Don't need 'FileUtils.' before #{$1}."
    end
Jack Nagel's avatar
Jack Nagel committed
    # Check for long inreplace block vars
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /inreplace .* do \|(.{2,})\|/
Jack Nagel's avatar
Jack Nagel committed
      problem "\"inreplace <filenames> do |s|\" is preferred over \"|#{$1}|\"."
    end
Jack Nagel's avatar
Jack Nagel committed
    # Check for string interpolation of single values.
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /(system|inreplace|gsub!|change_make_var!).*[ ,]"#\{([\w.]+)\}"/
Jack Nagel's avatar
Jack Nagel committed
      problem "Don't need to interpolate \"#{$2}\" with #{$1}"
    end
Jack Nagel's avatar
Jack Nagel committed
    # Check for string concatenation; prefer interpolation
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /(#\{\w+\s*\+\s*['"][^}]+\})/
Jack Nagel's avatar
Jack Nagel committed
      problem "Try not to concatenate paths in string interpolation:\n   #{$1}"
Jack Nagel's avatar
Jack Nagel committed
    # Prefer formula path shortcuts in Pathname+
    if line =~ %r{\(\s*(prefix\s*\+\s*(['"])(bin|include|libexec|lib|sbin|share|Frameworks)[/'"])}
      problem "\"(#{$1}...#{$2})\" should be \"(#{$3.downcase}+...)\""
Jack Nagel's avatar
Jack Nagel committed
    end
Jack Nagel's avatar
Jack Nagel committed

Jack Nagel's avatar
Jack Nagel committed
    if line =~ %r[((man)\s*\+\s*(['"])(man[1-8])(['"]))]
Jack Nagel's avatar
Jack Nagel committed
      problem "\"#{$1}\" should be \"#{$4}\""
    end
Jack Nagel's avatar
Jack Nagel committed
    # Prefer formula path shortcuts in strings
    if line =~ %r[(\#\{prefix\}/(bin|include|libexec|lib|sbin|share|Frameworks))]
      problem "\"#{$1}\" should be \"\#{#{$2.downcase}}\""
Jack Nagel's avatar
Jack Nagel committed
    if line =~ %r[((\#\{prefix\}/share/man/|\#\{man\}/)(man[1-8]))]
Jack Nagel's avatar
Jack Nagel committed
      problem "\"#{$1}\" should be \"\#{#{$3}}\""
    end
Jack Nagel's avatar
Jack Nagel committed
    if line =~ %r[((\#\{share\}/(man)))[/'"]]
Jack Nagel's avatar
Jack Nagel committed
      problem "\"#{$1}\" should be \"\#{#{$3}}\""
    end
Jack Nagel's avatar
Jack Nagel committed
    if line =~ %r[(\#\{prefix\}/share/(info|man))]
Jack Nagel's avatar
Jack Nagel committed
      problem "\"#{$1}\" should be \"\#{#{$2}}\""
    end
Jack Nagel's avatar
Jack Nagel committed
    # Commented-out depends_on
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /#\s*depends_on\s+(.+)\s*$/
      problem "Commented-out dep #{$1}"
Jack Nagel's avatar
Jack Nagel committed
    # No trailing whitespace, please
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /[\t ]+$/
      problem "#{lineno}: Trailing whitespace was found"
Jack Nagel's avatar
Jack Nagel committed
    end
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /if\s+ARGV\.include\?\s+'--(HEAD|devel)'/
Jack Nagel's avatar
Jack Nagel committed
      problem "Use \"if build.#{$1.downcase}?\" instead"
Jack Nagel's avatar
Jack Nagel committed
    end
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /make && make/
      problem "Use separate make calls"
Jack Nagel's avatar
Jack Nagel committed
    end
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /^[ ]*\t/
Jack Nagel's avatar
Jack Nagel committed
      problem "Use spaces instead of tabs for indentation"
    end
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /ENV\.x11/
Adam Vandenberg's avatar
Adam Vandenberg committed
      problem "Use \"depends_on :x11\" instead of \"ENV.x11\""
    end

Jack Nagel's avatar
Jack Nagel committed
    # Avoid hard-coding compilers
    if line =~ %r{(system|ENV\[.+\]\s?=)\s?['"](/usr/bin/)?(gcc|llvm-gcc|clang)['" ]}
      problem "Use \"\#{ENV.cc}\" instead of hard-coding \"#{$3}\""
Jack Nagel's avatar
Jack Nagel committed
    end
Jack Nagel's avatar
Jack Nagel committed
    if line =~ %r{(system|ENV\[.+\]\s?=)\s?['"](/usr/bin/)?((g|llvm-g|clang)\+\+)['" ]}
Jack Nagel's avatar
Jack Nagel committed
      problem "Use \"\#{ENV.cxx}\" instead of hard-coding \"#{$3}\""
    end
Adam Vandenberg's avatar
Adam Vandenberg committed
    if line =~ /system\s+['"](env|export)(\s+|['"])/
Jack Nagel's avatar
Jack Nagel committed
      problem "Use ENV instead of invoking '#{$1}' to modify the environment"
    end
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /version == ['"]HEAD['"]/
      problem "Use 'build.head?' instead of inspecting 'version'"
    end

    if line =~ /build\.include\?[\s\(]+['"]\-\-(.*)['"]/
      problem "Reference '#{$1}' without dashes"
    if line =~ /build\.include\?[\s\(]+['"]with(out)?-(.*)['"]/
      problem "Use build.with#{$1}? \"#{$2}\" instead of build.include? 'with#{$1}-#{$2}'"
    if line =~ /build\.with\?[\s\(]+['"]-?-?with-(.*)['"]/
      problem "Don't duplicate 'with': Use `build.with? \"#{$1}\"` to check for \"--with-#{$1}\""
    end

    if line =~ /build\.without\?[\s\(]+['"]-?-?without-(.*)['"]/
      problem "Don't duplicate 'without': Use `build.without? \"#{$1}\"` to check for \"--without-#{$1}\""
    end

    if line =~ /unless build\.with\?(.*)/
      problem "Use if build.without?#{$1} instead of unless build.with?#{$1}"
    end

    if line =~ /unless build\.without\?(.*)/
      problem "Use if build.with?#{$1} instead of unless build.without?#{$1}"
    end

    if line =~ /(not\s|!)\s*build\.with?\?/
      problem "Don't negate 'build.without?': use 'build.with?'"
    end

    if line =~ /(not\s|!)\s*build\.without?\?/
      problem "Don't negate 'build.with?': use 'build.without?'"
    if line =~ /ARGV\.(?!(debug\?|verbose\?|value[\(\s]))/
Jack Nagel's avatar
Jack Nagel committed
      problem "Use build instead of ARGV to check options"
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /def options/
      problem "Use new-style option definitions"
Jack Nagel's avatar
Jack Nagel committed
    end
    if line =~ /def test$/
Adam Vandenberg's avatar
Adam Vandenberg committed
      problem "Use new-style test definitions (test do)"
    end

Jack Nagel's avatar
Jack Nagel committed
    if line =~ /MACOS_VERSION/
      problem "Use MacOS.version instead of MACOS_VERSION"
    end
Jack Nagel's avatar
Jack Nagel committed
    cats = %w{leopard snow_leopard lion mountain_lion}.join("|")
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /MacOS\.(?:#{cats})\?/
Jack Nagel's avatar
Jack Nagel committed
      problem "\"#{$&}\" is deprecated, use a comparison to MacOS.version instead"
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /skip_clean\s+:all/
Adam Vandenberg's avatar
Adam Vandenberg committed
      problem "`skip_clean :all` is deprecated; brew no longer strips symbols\n" +
              "\tPass explicit paths to prevent Homebrew from removing empty folders."
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /depends_on [A-Z][\w:]+\.new$/
Jack Nagel's avatar
Jack Nagel committed
      problem "`depends_on` can take requirement classes instead of instances"
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /^def (\w+).*$/
      problem "Define method #{$1.inspect} in the class body, not at the top-level"
    end
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /ENV.fortran/
      problem "Use `depends_on :fortran` instead of `ENV.fortran`"
    end
Jack Nagel's avatar
Jack Nagel committed
    if line =~ /depends_on :(.+) (if.+|unless.+)$/
      audit_conditional_dep($1.to_sym, $2, $&)
    end

Jack Nagel's avatar
Jack Nagel committed
    if line =~ /depends_on ['"](.+)['"] (if.+|unless.+)$/
      audit_conditional_dep($1, $2, $&)
    end

    if line =~ /(Dir\[("[^\*{},]+")\])/
      problem "#{$1} is unnecessary; just use #{$2}"
    end
    if line =~ /system (["'](#{FILEUTILS_METHODS})["' ])/o
      system = $1
      method = $2
      problem "Use the `#{method}` Ruby method instead of `system #{system}`"
    end
      if line =~ /system (["'][^"' ]*(?:\s[^"' ]*)+["'])/
        bad_system = $1
        good_system = bad_system.gsub(" ", "\", \"")
        problem "Use `system #{good_system}` instead of `system #{bad_system}` "
      end
      if line =~ /(require ["']formula["'])/
        problem "`#{$1}` is now unnecessary"
      end
  def audit_caveats
    caveats = formula.caveats

    if caveats =~ /setuid/
      problem "Don't recommend setuid in the caveats, suggest sudo instead."
    end
  end

  def audit_prefix_has_contents
    return unless formula.prefix.directory?

    Pathname.glob("#{formula.prefix}/**/*") do |file|
      next if file.directory?
      basename = file.basename.to_s
      next if Metafiles.copy?(basename)
      next if %w[.DS_Store INSTALL_RECEIPT.json].include?(basename)
      return
    end

    problem <<-EOS.undent
      The installation seems to be empty. Please ensure the prefix
      is set correctly and expected files are installed.
      The prefix configure/make argument may be case-sensitive.
    EOS
  end

  def audit_conditional_dep(dep, condition, line)
Jack Nagel's avatar
Jack Nagel committed
    quoted_dep = quote_dep(dep)
    dep = Regexp.escape(dep.to_s)

    case condition
    when /if build\.include\? ['"]with-#{dep}['"]$/, /if build\.with\? ['"]#{dep}['"]$/
Jack Nagel's avatar
Jack Nagel committed
      problem %{Replace #{line.inspect} with "depends_on #{quoted_dep} => :optional"}
    when /unless build\.include\? ['"]without-#{dep}['"]$/, /unless build\.without\? ['"]#{dep}['"]$/
Jack Nagel's avatar
Jack Nagel committed
      problem %{Replace #{line.inspect} with "depends_on #{quoted_dep} => :recommended"}
    end
  end

  def quote_dep(dep)
    Symbol === dep ? dep.inspect : "'#{dep}'"
Jack Nagel's avatar
Jack Nagel committed
  end
  def audit_check_output(output)
    problem(output) if output
Jack Nagel's avatar
Jack Nagel committed
  def audit
    audit_file
    audit_class
Jack Nagel's avatar
Jack Nagel committed
    audit_specs
    audit_urls
    audit_deps
    audit_conflicts
    audit_options
Jack Nagel's avatar
Jack Nagel committed
    audit_patches
Jack Nagel's avatar
Jack Nagel committed
    audit_text
    audit_caveats
    text.without_patch.split("\n").each_with_index { |line, lineno| audit_line(line, lineno+1) }
    audit_installed
    audit_prefix_has_contents
Jack Nagel's avatar
Jack Nagel committed
  end
Max Howell's avatar
Max Howell committed

Jack Nagel's avatar
Jack Nagel committed
  private
Jack Nagel's avatar
Jack Nagel committed
  def problem p
Jack Nagel's avatar
Jack Nagel committed
    @problems << p
    formula.head && formula.devel.nil? && formula.stable.nil?
  end

  def devel_only?(formula)
    formula.devel && formula.stable.nil?

class ResourceAuditor
  attr_reader :problems
  attr_reader :version, :checksum, :using, :specs, :url, :name

  def initialize(resource)
    @version  = resource.version
    @checksum = resource.checksum
    @url      = resource.url
    @using    = resource.using
    @specs    = resource.specs
    @problems = []
  end

  def audit
    audit_version
    audit_checksum
    audit_download_strategy
    self
  end

  def audit_version
    if version.nil?
      problem "missing version"
    elsif version.to_s.empty?
      problem "version is set to an empty string"
    elsif not version.detected_from_url?
      version_text = version
      version_url = Version.detect(url, specs)
      if version_url.to_s == version_text.to_s && version.instance_of?(Version)
        problem "version #{version_text} is redundant with version scanned from URL"
      end
    end

    if version.to_s =~ /^v/
      problem "version #{version} should not have a leading 'v'"
    end
  end

  def audit_checksum
    return unless checksum

    case checksum.hash_type
    when :md5
      problem "MD5 checksums are deprecated, please use SHA1 or SHA256"
      return
    when :sha1   then len = 40
    when :sha256 then len = 64
    end

    if checksum.empty?
      problem "#{checksum.hash_type} is empty"
    else
      problem "#{checksum.hash_type} should be #{len} characters" unless checksum.hexdigest.length == len
      problem "#{checksum.hash_type} contains invalid characters" unless checksum.hexdigest =~ /^[a-fA-F0-9]+$/
      problem "#{checksum.hash_type} should be lowercase" unless checksum.hexdigest == checksum.hexdigest.downcase
    end
  end

  def audit_download_strategy
    if url =~ %r[^(cvs|bzr|hg|fossil)://] || url =~ %r[^(svn)\+http://]
      problem "Use of the #{$&} scheme is deprecated, pass `:using => :#{$1}` instead"
    end

    return unless using

    if using == :ssl3 || using == CurlSSL3DownloadStrategy
      problem "The SSL3 download strategy is deprecated, please choose a different URL"
    elsif using == CurlUnsafeDownloadStrategy || using == UnsafeSubversionDownloadStrategy
      problem "#{using.name} is deprecated, please choose a different URL"
    end

    if using == :cvs
      mod = specs[:module]

      if mod == name
        problem "Redundant :module value in URL"
      end

      if url =~ %r[:[^/]+$]
        mod = url.split(":").last

        if mod == name
          problem "Redundant CVS module appended to URL"
        else
          problem "Specify CVS module as `:module => \"#{mod}\"` instead of appending it to the URL"
        end
      end
    end

    url_strategy   = DownloadStrategyDetector.detect(url)
    using_strategy = DownloadStrategyDetector.detect('', using)

    if url_strategy == using_strategy
      problem "Redundant :using value in URL"
    end
  end

  def problem text
    @problems << text
  end
end