Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Refactor `brew audit`

  • Loading branch information...
commit b9105dc5ebfd2102435d15fbe0bd6394784bd88d 1 parent 66a246f
@jacknagel authored
Showing with 298 additions and 282 deletions.
  1. +296 −282 Library/Homebrew/cmd/audit.rb
  2. +2 −0  Library/Homebrew/patches.rb
View
578 Library/Homebrew/cmd/audit.rb
@@ -2,9 +2,35 @@
require 'utils'
require 'extend/ENV'
-# Some formulae use ENV in patches, so set up an environment
-ENV.extend(HomebrewEnvExtension)
-ENV.setup_build_environment
+module Homebrew extend self
+ def audit
+ formula_count = 0
+ problem_count = 0
+
+ ff = if ARGV.named.empty?
+ Formula.all
+ else
+ ARGV.formulae
+ end
+
+ ff.each do |f|
+ fa = FormulaAuditor.new f
+ fa.audit
+
+ unless fa.problems.empty?
+ puts "#{f.name}:"
+ fa.problems.each { |p| puts " * #{p}" }
+ puts
+ formula_count += 1
+ problem_count += fa.problems.size
+ end
+ end
+
+ unless problem_count.zero?
+ ofail "#{problem_count} problems in #{formula_count} formulae"
+ end
+ end
+end
class Module
def redefine_const(name, value)
@@ -13,365 +39,353 @@ def redefine_const(name, value)
end
end
-def ff
- return Formula.all if ARGV.named.empty?
- return ARGV.formulae
-end
-
-def audit_formula_text name, text
- problems = []
-
- if text =~ /<(Formula|AmazonWebServicesFormula|ScriptFileFormula|GithubGistFormula)/
- problems << " * Use a space in class inheritance: class Foo < #{$1}"
+# Formula extensions for auditing
+class Formula
+ def head_only?
+ @head and @stable.nil?
end
- # Commented-out cmake support from default template
- if (text =~ /# depends_on 'cmake'/) or (text =~ /# system "cmake/)
- problems << " * Commented cmake support found."
+ def text
+ @text ||= FormulaText.new(@path)
end
+end
- # 2 (or more in an if block) spaces before depends_on, please
- if text =~ /^\ ?depends_on/
- problems << " * Check indentation of 'depends_on'."
+class FormulaText
+ def initialize path
+ @text = path.open('r') { |f| f.read }
end
- # build tools should be flagged properly
- build_deps = %w{autoconf automake boost-build bsdmake
- cmake imake libtool pkg-config scons smake}
- if text =~ /depends_on ['"](#{build_deps*'|'})['"]$/
- problems << " * #{$1} dependency should be \"depends_on '#{$1}' => :build\""
+ def without_patch
+ @text.split("__END__")[0].strip()
end
- # FileUtils is included in Formula
- if text =~ /FileUtils\.(\w+)/
- problems << " * Don't need 'FileUtils.' before #{$1}."
+ def has_DATA?
+ /\bDATA\b/ =~ @text
end
- # Check for long inreplace block vars
- if text =~ /inreplace .* do \|(.{2,})\|/
- problems << " * \"inreplace <filenames> do |s|\" is preferred over \"|#{$1}|\"."
+ def has_END?
+ /^__END__$/ =~ @text
end
- # Check for string interpolation of single values.
- if text =~ /(system|inreplace|gsub!|change_make_var!) .* ['"]#\{(\w+(\.\w+)?)\}['"]/
- problems << " * Don't need to interpolate \"#{$2}\" with #{$1}"
+ def has_trailing_newline?
+ /.+\z/ =~ @text
end
+end
- # Check for string concatenation; prefer interpolation
- if text =~ /(#\{\w+\s*\+\s*['"][^}]+\})/
- problems << " * Try not to concatenate paths in string interpolation:\n #{$1}"
- end
+class FormulaAuditor
+ attr :f
+ attr :text
+ attr :problems, true
+
+ BUILD_TIME_DEPS = %W[
+ autoconf
+ automake
+ boost-build
+ bsdmake
+ cmake
+ imake
+ libtool
+ pkg-config
+ scons
+ smake
+ ]
+
+ def initialize f
+ @f = f
+ @problems = []
+ @text = f.text.without_patch
+
+ # We need to do this in case the formula defines a patch that uses DATA.
+ f.class.redefine_const :DATA, ""
+ end
+
+ def audit_file
+ unless f.path.stat.mode.to_s(8) == "100644"
+ problem "Incorrect file permissions: chmod 644 #{f.path}"
+ end
- # Prefer formula path shortcuts in Pathname+
- if text =~ %r{\(\s*(prefix\s*\+\s*(['"])(bin|include|libexec|lib|sbin|share)[/'"])}
- problems << " * \"(#{$1}...#{$2})\" should be \"(#{$3}+...)\""
- end
+ if f.text.has_DATA? and not f.text.has_END?
+ problem "'DATA' was found, but no '__END__'"
+ end
- if text =~ %r[((man)\s*\+\s*(['"])(man[1-8])(['"]))]
- problems << " * \"#{$1}\" should be \"#{$4}\""
- end
+ if f.text.has_END? and not f.text.has_DATA?
+ problem "'__END__' was found, but 'DATA' is not used"
+ end
- # Prefer formula path shortcuts in strings
- if text =~ %r[(\#\{prefix\}/(bin|include|libexec|lib|sbin|share))]
- problems << " * \"#{$1}\" should be \"\#{#{$2}}\""
+ if f.text.has_trailing_newline?
+ problem "File should end with a newline"
+ end
end
- if text =~ %r[((\#\{prefix\}/share/man/|\#\{man\}/)(man[1-8]))]
- problems << " * \"#{$1}\" should be \"\#{#{$3}}\""
- end
+ def audit_deps
+ problems = []
- if text =~ %r[((\#\{share\}/(man)))[/'"]]
- problems << " * \"#{$1}\" should be \"\#{#{$3}}\""
- end
+ # Don't depend_on aliases; use full name
+ aliases = Formula.aliases
+ f.deps.select {|d| aliases.include? d}.each do |d|
+ problem "Dependency #{d} is an alias; use the canonical name."
+ end
- if text =~ %r[(\#\{prefix\}/share/(info|man))]
- problems << " * \"#{$1}\" should be \"\#{#{$2}}\""
- end
+ # Check for things we don't like to depend on.
+ # We allow non-Homebrew installs whenever possible.
+ f.deps.each do |d|
+ begin
+ dep_f = Formula.factory d
+ rescue
+ problem "Can't find dependency \"#{d}\"."
+ end
- # Commented-out depends_on
- if text =~ /#\s*depends_on\s+(.+)\s*$/
- problems << " * Commented-out dep #{$1}."
+ case d.name
+ when "git", "python", "ruby", "emacs", "mysql", "postgresql", "mercurial"
+ problem <<-EOS.undent
+ Don't use #{d} as a dependency. We allow non-Homebrew
+ #{d} installations.
+ EOS
+ when 'gfortran'
+ problem "Use ENV.fortran during install 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 MPIDependency.new(<lang list>)
+ Where <lang list> is a comma delimited list that can include:
+ :cc, :cxx, :f90, :f77
+ EOS
+ end
+ end
end
- # No trailing whitespace, please
- if text =~ /(\t|[ ])+$/
- problems << " * Trailing whitespace was found."
- end
- if text =~ /if\s+ARGV\.include\?\s+'--(HEAD|devel)'/
- problems << " * Use \"if ARGV.build_#{$1.downcase}?\" instead"
- end
+ def audit_urls
+ unless f.homepage =~ %r[^https?://]
+ problem "The homepage should start with http or https."
+ end
- if text =~ /make && make/
- problems << " * Use separate make calls."
- end
+ # Google Code homepages should end in a slash
+ if f.homepage =~ %r[^https?://code\.google\.com/p/[^/]+[^/]$]
+ problem "Google Code homepage should end with a slash."
+ end
- if text =~ /^[ ]*\t/
- problems << " * Use spaces instead of tabs for indentation"
- end
+ urls = [(f.stable.url rescue nil), (f.devel.url rescue nil), (f.head.url rescue nil)].compact
- # xcodebuild should specify SYMROOT
- if text =~ /system\s+['"]xcodebuild/ and not text =~ /SYMROOT=/
- problems << " * xcodebuild should be passed an explicit \"SYMROOT\""
- end
+ # Check GNU urls; doesn't apply to mirrors
+ if urls.any? { |p| p =~ %r[^(https?|ftp)://(.+)/gnu/] }
+ problem "\"ftpmirror.gnu.org\" is preferred for GNU software."
+ end
- # using ARGV.flag? for formula options is generally a bad thing
- if text =~ /ARGV\.flag\?/
- problems << " * Use 'ARGV.include?' instead of 'ARGV.flag?'"
- end
+ # the rest of the checks apply to mirrors as well
+ urls.concat([(f.stable.mirrors rescue nil), (f.devel.mirrors rescue nil)].flatten.compact)
- # Avoid hard-coding compilers
- if text =~ %r[(system|ENV\[.+\]\s?=)\s?['"](/usr/bin/)?(gcc|llvm-gcc|clang)['" ]]
- problems << " * Use \"\#{ENV.cc}\" instead of hard-coding \"#{$3}\""
- end
+ # Check SourceForge urls
+ urls.each do |p|
+ # Is it a filedownload (instead of svnroot)
+ next if p =~ %r[/svnroot/]
+ next if p =~ %r[svn\.sourceforge]
- if text =~ %r[(system|ENV\[.+\]\s?=)\s?['"](/usr/bin/)?((g|llvm-g|clang)\+\+)['" ]]
- problems << " * Use \"\#{ENV.cxx}\" instead of hard-coding \"#{$3}\""
- end
+ # Is it a sourceforge http(s) URL?
+ next unless p =~ %r[^https?://.*\bsourceforge\.]
- if text =~ /system\s+['"](env|export)/
- problems << " * Use ENV instead of invoking '#{$1}' to modify the environment"
- end
+ if p =~ /(\?|&)use_mirror=/
+ problem "Update this url (don't use #{$1}use_mirror)."
+ end
- if text =~ /version == ['"]HEAD['"]/
- problems << " * Use 'ARGV.build_head?' instead of inspecting 'version'"
- end
+ if p =~ /\/download$/
+ problem "Update this url (don't use /download)."
+ end
- return problems
-end
+ if p =~ %r[^http://prdownloads\.]
+ problem "Update this url (don't use prdownloads)."
+ end
-INSTALL_OPTIONS = %W[
- --build-from-source
- --debug
- --devel
- --force
- --fresh
- --HEAD
- --ignore-dependencies
- --interactive
- --use-clang
- --use-gcc
- --use-llvm
- --verbose
-].freeze
-
-def audit_formula_patches f
- problems = []
- patches = Patches.new(f.patches)
- patches.each do |p|
- next unless p.external?
- case p.url
- when %r[raw\.github\.com], %r[gist\.github\.com/raw]
- problems << " * Using raw GitHub URLs is not recommended:"
- problems << " #{p.url}"
- when %r[macports/trunk]
- problems << " * MacPorts patches should specify a revision instead of trunk:"
- problems << " #{p.url}"
+ if p =~ %r[^http://\w+\.dl\.]
+ problem "Update this url (don't use specific dl mirrors)."
+ end
end
- end
- return problems
-end
-
-def audit_formula_urls f
- problems = []
- unless f.homepage =~ %r[^https?://]
- problems << " * The homepage should start with http or https."
+ # Check for git:// urls; https:// is preferred.
+ if urls.any? { |p| p =~ %r[^git://github\.com/] }
+ problem "Use https:// URLs for accessing GitHub repositories."
+ end
end
- # Google Code homepages should end in a slash
- if f.homepage =~ %r[^https?://code\.google\.com/p/[^/]+[^/]$]
- problems << " * Google Code homepage should end with a slash."
- end
+ def audit_specs
+ problem "Head-only (no stable download)" if f.head_only?
- urls = [(f.stable.url rescue nil), (f.devel.url rescue nil), (f.head.url rescue nil)].reject {|p| p.nil?}
+ [:stable, :devel].each do |spec|
+ s = f.send(spec)
+ next if s.nil?
+
+ if s.version.to_s.empty?
+ problem "Invalid or missing #{spec} version"
+ else
+ version_text = s.version if s.explicit_version?
+ version_url = Pathname.new(s.url).version
+ if version_url == version_text
+ problem "#{spec} version #{version_text} is redundant with version scanned from URL"
+ end
+ end
- # Check GNU urls; doesn't apply to mirrors
- urls.each do |p|
- if p =~ %r[^(https?|ftp)://(.+)/gnu/]
- problems << " * \"ftpmirror.gnu.org\" is preferred for GNU software."
+ cksum = s.checksum
+ next if cksum.nil?
+
+ len = case cksum.hash_type
+ when :md5 then 32
+ when :sha1 then 40
+ when :sha256 then 64
+ end
+
+ if cksum.empty?
+ problem "#{cksum.hash_type} is empty"
+ else
+ problem "#{cksum.hash_type} should be #{len} characters" unless cksum.hexdigest.length == len
+ problem "#{cksum.hash_type} contains invalid characters" unless cksum.hexdigest =~ /^[a-fA-F0-9]+$/
+ problem "#{cksum.hash_type} should be lowercase" unless cksum.hexdigest == cksum.hexdigest.downcase
+ end
end
end
- # the rest of the checks apply to mirrors as well
- mirrors = [(f.stable.mirrors rescue []), (f.devel.mirrors rescue [])].flatten.reject { |m| m.nil? }
- urls.concat mirrors.map { |m| m[:url] }
-
- # Check SourceForge urls
- urls.each do |p|
- # Is it a filedownload (instead of svnroot)
- next if p =~ %r[/svnroot/]
- next if p =~ %r[svn\.sourceforge]
+ def audit_patches
+ # Some formulae use ENV in patches, so set up an environment
+ ENV.extend(HomebrewEnvExtension)
+ ENV.setup_build_environment
- # Is it a sourceforge http(s) URL?
- next unless p =~ %r[^https?://.*\bsourceforge\.]
-
- if p =~ /(\?|&)use_mirror=/
- problems << " * Update this url (don't use #{$1}use_mirror)."
+ Patches.new(f.patches).select { |p| p.external? }.each do |p|
+ case p.url
+ when %r[raw\.github\.com], %r[gist\.github\.com/raw]
+ problem "Using raw GitHub URLs is not recommended:\n#{p.url}"
+ when %r[macports/trunk]
+ problem "MacPorts patches should specify a revision instead of trunk:\n#{p.url}"
+ end
end
+ end
- if p =~ /\/download$/
- problems << " * Update this url (don't use /download)."
+ def audit_text
+ if text =~ /<(Formula|AmazonWebServicesFormula|ScriptFileFormula|GithubGistFormula)/
+ problem "Use a space in class inheritance: class Foo < #{$1}"
end
- if p =~ %r[^http://prdownloads\.]
- problems << " * Update this url (don't use prdownloads)."
+ # Commented-out cmake support from default template
+ if (text =~ /# depends_on 'cmake'/) or (text =~ /# system "cmake/)
+ problem "Commented cmake support found."
end
- if p =~ %r[^http://\w+\.dl\.]
- problems << " * Update this url (don't use specific dl mirrors)."
+ # 2 (or more in an if block) spaces before depends_on, please
+ if text =~ /^\ ?depends_on/
+ problem "Check indentation of 'depends_on'."
end
- end
- # Check for git:// urls; https:// is preferred.
- urls.each do |p|
- if p =~ %r[^git://github\.com/]
- problems << " * Use https:// URLs for accessing repositories on GitHub."
+ # build tools should be flagged properly
+ if text =~ /depends_on ['"](#{BUILD_TIME_DEPS*'|'})['"]$/
+ problem "#{$1} dependency should be \"depends_on '#{$1}' => :build\""
end
- end
- return problems
-end
+ # FileUtils is included in Formula
+ if text =~ /FileUtils\.(\w+)/
+ problem "Don't need 'FileUtils.' before #{$1}."
+ end
-def audit_formula_specs f
- problems = []
+ # Check for long inreplace block vars
+ if text =~ /inreplace .* do \|(.{2,})\|/
+ problem "\"inreplace <filenames> do |s|\" is preferred over \"|#{$1}|\"."
+ end
- [:stable, :devel].each do |spec|
- s = f.send(spec)
- next if s.nil?
+ # Check for string interpolation of single values.
+ if text =~ /(system|inreplace|gsub!|change_make_var!) .* ['"]#\{(\w+(\.\w+)?)\}['"]/
+ problem "Don't need to interpolate \"#{$2}\" with #{$1}"
+ end
- if s.version.to_s.empty?
- problems << " * invalid or missing #{spec} version"
- else
- version_text = s.version if s.explicit_version?
- version_url = Pathname.new(s.url).version
- if version_url == version_text
- problems << " * #{spec} version #{version_text} is redundant with version scanned from URL"
- end
+ # Check for string concatenation; prefer interpolation
+ if text =~ /(#\{\w+\s*\+\s*['"][^}]+\})/
+ problem "Try not to concatenate paths in string interpolation:\n #{$1}"
end
- cksum = s.checksum
- next if cksum.nil?
+ # Prefer formula path shortcuts in Pathname+
+ if text =~ %r{\(\s*(prefix\s*\+\s*(['"])(bin|include|libexec|lib|sbin|share)[/'"])}
+ problem "\"(#{$1}...#{$2})\" should be \"(#{$3}+...)\""
+ end
- len = case cksum.hash_type
- when :md5 then 32
- when :sha1 then 40
- when :sha256 then 64
- end
+ if text =~ %r[((man)\s*\+\s*(['"])(man[1-8])(['"]))]
+ problem "\"#{$1}\" should be \"#{$4}\""
+ end
- if cksum.empty?
- problems << " * #{cksum.hash_type} is empty"
- else
- problems << " * #{cksum.hash_type} should be #{len} characters" unless cksum.hexdigest.length == len
- problems << " * #{cksum.hash_type} contains invalid characters" unless cksum.hexdigest =~ /^[a-fA-F0-9]+$/
- problems << " * #{cksum.hash_type} should be lowercase" unless cksum.hexdigest == cksum.hexdigest.downcase
+ # Prefer formula path shortcuts in strings
+ if text =~ %r[(\#\{prefix\}/(bin|include|libexec|lib|sbin|share))]
+ problem "\"#{$1}\" should be \"\#{#{$2}}\""
end
- end
- return problems
-end
+ if text =~ %r[((\#\{prefix\}/share/man/|\#\{man\}/)(man[1-8]))]
+ problem "\"#{$1}\" should be \"\#{#{$3}}\""
+ end
-def audit_formula_instance f
- problems = []
+ if text =~ %r[((\#\{share\}/(man)))[/'"]]
+ problem "\"#{$1}\" should be \"\#{#{$3}}\""
+ end
- # Don't depend_on aliases; use full name
- aliases = Formula.aliases
- f.deps.select {|d| aliases.include? d}.each do |d|
- problems << " * Dep #{d} is an alias; switch to the real name."
- end
+ if text =~ %r[(\#\{prefix\}/share/(info|man))]
+ problem "\"#{$1}\" should be \"\#{#{$2}}\""
+ end
- # Check for things we don't like to depend on.
- # We allow non-Homebrew installs whenever possible.
- f.deps.each do |d|
- begin
- dep_f = Formula.factory d
- rescue
- problems << " * Can't find dependency \"#{d}\"."
- end
-
- case d.name
- when "git", "python", "ruby", "emacs", "mysql", "postgresql", "mercurial"
- problems << <<-EOS
- * Don't use #{d} as a dependency. We allow non-Homebrew
- #{d} installations.
-EOS
- when 'gfortran'
- problems << " * Use ENV.fortran during install instead of depends_on 'gfortran'"
-
- when 'open-mpi', 'mpich2'
- problems << <<-EOS.undent
- * There are multiple conflicting ways to install MPI. Use a MPIDependency:
- depends_on MPIDependency.new(<lang list>)
- Where <lang list> is a comma delimited list that can include:
- :cc, :cxx, :f90, :f77
- EOS
+ # Commented-out depends_on
+ if text =~ /#\s*depends_on\s+(.+)\s*$/
+ problem "Commented-out dep #{$1}."
end
- end
- return problems
-end
+ # No trailing whitespace, please
+ if text =~ /(\t|[ ])+$/
+ problem "Trailing whitespace was found."
+ end
-# Formula extensions for auditing
-class Formula
- def head_only?
- @head and @stable.nil?
- end
+ if text =~ /if\s+ARGV\.include\?\s+'--(HEAD|devel)'/
+ problem "Use \"if ARGV.build_#{$1.downcase}?\" instead"
+ end
- def formula_text
- File.open(@path, "r") { |afile| return afile.read }
- end
-end
+ if text =~ /make && make/
+ problem "Use separate make calls."
+ end
-module Homebrew extend self
- def audit
- errors = false
+ if text =~ /^[ ]*\t/
+ problem "Use spaces instead of tabs for indentation"
+ end
- brew_count = 0
- problem_count = 0
+ # xcodebuild should specify SYMROOT
+ if text =~ /system\s+['"]xcodebuild/ and not text =~ /SYMROOT=/
+ problem "xcodebuild should be passed an explicit \"SYMROOT\""
+ end
- ff.each do |f|
- # We need to do this in case the formula defines a patch that uses DATA.
- f.class.redefine_const :DATA, ""
-
- problems = []
- problems += [' * head-only formula'] if f.head_only?
- problems += audit_formula_instance f
- problems += audit_formula_urls f
- problems += audit_formula_patches f
-
- perms = File.stat(f.path).mode
- if perms.to_s(8) != "100644"
- problems << " * permissions wrong; chmod 644 #{f.path}"
- end
+ # using ARGV.flag? for formula options is generally a bad thing
+ if text =~ /ARGV\.flag\?/
+ problem "Use 'ARGV.include?' instead of 'ARGV.flag?'"
+ end
- text = f.formula_text
+ # Avoid hard-coding compilers
+ if text =~ %r[(system|ENV\[.+\]\s?=)\s?['"](/usr/bin/)?(gcc|llvm-gcc|clang)['" ]]
+ problem "Use \"\#{ENV.cc}\" instead of hard-coding \"#{$3}\""
+ end
- # DATA with no __END__
- if (text =~ /\bDATA\b/) and not (text =~ /^\s*__END__\s*$/)
- problems << " * 'DATA' was found, but no '__END__'"
- end
+ if text =~ %r[(system|ENV\[.+\]\s?=)\s?['"](/usr/bin/)?((g|llvm-g|clang)\+\+)['" ]]
+ problem "Use \"\#{ENV.cxx}\" instead of hard-coding \"#{$3}\""
+ end
- # files should end with a newline
- if text =~ /.+\z/
- problems << " * File should end with a newline"
- end
+ if text =~ /system\s+['"](env|export)/
+ problem "Use ENV instead of invoking '#{$1}' to modify the environment"
+ end
- # Don't try remaining audits on text in __END__
- text_without_patch = (text.split("__END__")[0]).strip()
+ if text =~ /version == ['"]HEAD['"]/
+ problem "Use 'ARGV.build_head?' instead of inspecting 'version'"
+ end
+ end
- problems += audit_formula_text(f.name, text_without_patch)
- problems += audit_formula_specs(f)
+ def audit
+ audit_file
+ audit_specs
+ audit_urls
+ audit_deps
+ audit_patches
+ audit_text
+ end
- unless problems.empty?
- errors = true
- puts "#{f.name}:"
- puts problems * "\n"
- puts
- brew_count += 1
- problem_count += problems.select{ |p| p.start_with? ' *' }.size
- end
- end
+ private
- ofail "#{problem_count} problems in #{brew_count} brews" if errors
+ def problem p
+ @problems << p
@mxcl
mxcl added a note

If we yielded instead, problems could be output as they are found, making it more responsive.

@jacknagel Owner

Yes, good idea.

@jacknagel Owner

Implemented this, and it is slightly more responsive but it makes e.g. omitting printing the formula name if there are no problems a bit more difficult. It can be done by yielding an index along with the problem string, but I don't know if it is worth it.

Of course, there is probably a better way to structure it.

@mxcl
mxcl added a note

Don't bother then I should think. It's not important that this is responsive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
end
end
View
2  Library/Homebrew/patches.rb
@@ -1,4 +1,6 @@
class Patches
+ include Enumerable
+
# The patches defined in a formula and the DATA from that file
def initialize patches
@patches = []
@mxcl

If we yielded instead, problems could be output as they are found, making it more responsive.

@jacknagel

Yes, good idea.

@jacknagel

Implemented this, and it is slightly more responsive but it makes e.g. omitting printing the formula name if there are no problems a bit more difficult. It can be done by yielding an index along with the problem string, but I don't know if it is worth it.

Of course, there is probably a better way to structure it.

@mxcl

Don't bother then I should think. It's not important that this is responsive.

Please sign in to comment.
Something went wrong with that request. Please try again.