class Brakeman::AppTree

def self.from_options(options)

def self.from_options(options)
  root = File.expand_path options[:app_path]
  # Convert files into Regexp for matching
  init_options = {}
  if options[:skip_files]
    init_options[:skip_files] = regex_for_paths(options[:skip_files])
  end
  if options[:only_files]
    init_options[:only_files] = regex_for_paths(options[:only_files])
  end
  init_options[:additional_libs_path] = options[:additional_libs_path]
  init_options[:engine_paths] = options[:engine_paths]
  init_options[:skip_vendor] = options[:skip_vendor]
  init_options[:follow_symlinks] = options[:follow_symlinks]
  new(root, init_options)
end

def self.regex_for_paths(paths)

TODO: This is wacky and I don't like it.

* "/path1/ - Matches any path that is rooted at "path1" in the project directory.
* "path1/" - Matches any path that contains "path1" in the project directory.
* "path1/file1.rb" - Matches a specific filename in the project directory.
returns a Regexp to match them:
Accepts an array of filenames and paths with the following format and
def self.regex_for_paths(paths)
  path_regexes = paths.map do |f|
    # If path ends in a file separator then we assume it is a path rather
    # than a filename.
    if f.end_with?(File::SEPARATOR)
      # If path starts with a file separator then we assume that they
      # want the project relative path to start with this path prefix.
      if f.start_with?(File::SEPARATOR)
        "\\A#{Regexp.escape f}"
      # If it ends in a file separator, but does not begin with a file
      # separator then we assume the path can match any path component in
      # the project.
      else
        Regexp.escape f
      end
    else
      "#{Regexp.escape f}\\z"
    end
  end
  Regexp.new("(?:#{path_regexes.join("|")})")
end

def controller_paths

def controller_paths
  @controller_paths ||= prioritize_concerns(find_paths("app/**/controllers"))
end

def convert_to_file_paths paths

def convert_to_file_paths paths
  paths.map { |path| file_path(path) }
end

def exists?(path)

def exists?(path)
  if path.is_a? Brakeman::FilePath
    path.exists?
  else
    File.exist?(File.join(@root, path))
  end
end

def expand_path(path)

Use AppTree#file_path(path).absolute instead.
Should only be used by Brakeman::FilePath.
def expand_path(path)
  File.expand_path(path, @root)
end

def file_path(path)

Create a new Brakeman::FilePath
def file_path(path)
  Brakeman::FilePath.from_app_tree(self, path)
end

def find_additional_lib_paths

def find_additional_lib_paths
  @additional_libs_path.collect{ |path| find_paths path }.flatten
end

def find_helper_paths

def find_helper_paths
  find_paths "app/helpers"
end

def find_job_paths

def find_job_paths
  find_paths "app/jobs"
end

def find_paths(directory, extensions = ".rb")

def find_paths(directory, extensions = ".rb")
  select_files(glob_files(directory, "*", extensions))
end

def gemspec

def gemspec
  return @gemspec unless @gemspec.nil?
  gemspecs =  Dir.glob(File.join(@root, "*.gemspec"))
  if gemspecs.length > 1 or gemspecs.empty?
    @gemspec = false
  else
    @gemspec = file_path(File.basename(gemspecs.first))
  end
end

def glob_files(directory, name, extensions = ".rb")

def glob_files(directory, name, extensions = ".rb")
  if @follow_symlinks
    root_directory = "#{root_search_pattern}#{directory}"
    patterns = ["#{root_directory}/**/#{name}#{extensions}"]
    Dir.glob("#{root_directory}/**/*", File::FNM_DOTMATCH).each do |path|
      if File.symlink?(path) && File.directory?(path)
        symlink_target = File.readlink(path)
        if Pathname.new(symlink_target).relative?
          symlink_target = File.join(File.dirname(path), symlink_target)
        end
        patterns << "#{search_pattern(symlink_target)}/**/#{name}#{extensions}"
      end
    end
    files = patterns.flat_map { |pattern| Dir.glob(pattern) }
    files.uniq.lazy
  else
    if directory == '.'
      pattern = File.join(top_directories_pattern, '**', "#{name}#{extensions}")
    else
      pattern = "#{root_search_pattern}#{directory}/**/#{name}#{extensions}"
    end
    Dir.glob(pattern).lazy
  end
end

def in_add_libs_paths?(path)

def in_add_libs_paths?(path)
  @additional_libs_path.any? { |p| path.absolute.include?(p) }
end

def in_engine_paths?(path)

def in_engine_paths?(path)
  @engine_paths.any? { |p| path.absolute.include?(p) }
end

def initialize(root, init_options = {})

def initialize(root, init_options = {})
  @root = root
  @project_root_path = Pathname.new(@root)
  @skip_files = init_options[:skip_files]
  @only_files = init_options[:only_files]
  @additional_libs_path = init_options[:additional_libs_path] || []
  @engine_paths = init_options[:engine_paths] || []
  @absolute_engine_paths = @engine_paths.select { |path| path.start_with?(File::SEPARATOR) }
  @relative_engine_paths = @engine_paths - @absolute_engine_paths
  @skip_vendor = init_options[:skip_vendor]
  @follow_symlinks = init_options[:follow_symlinks]
  @gemspec = nil
  @root_search_pattern = nil
end

def initializer_paths

def initializer_paths
  @initializer_paths ||= prioritize_concerns(find_paths("config/initializers"))
end

def layout_exists?(name)

def layout_exists?(name)
  !Dir.glob("#{root_search_pattern}app/views/layouts/#{name}.html.{erb,haml,slim}").empty?
end

def lib_paths

def lib_paths
  @lib_files ||= find_paths("lib").reject { |path| path.relative.include? "/generators/" or path.relative.include? "lib/tasks/" or path.relative.include? "lib/templates/" } +
                 find_additional_lib_paths +
                 find_helper_paths +
                 find_job_paths
end

def marshallable

Call this to be able to marshall the AppTree
def marshallable
  @initializer_paths = @initializer_paths.to_a
  @controller_paths = @controller_paths.to_a
  @template_paths = @template_paths.to_a
  @lib_files = @file_paths.to_a
  self
end

def match_path files, path

def match_path files, path
  # TODO: Converting to Pathnames and Strings seems like a lot
  # of converting that could perhaps all be handled in Brakeman::FilePath
  # instead?
  absolute_path = Pathname.new(path)
  # relative root never has a leading separator. But, we use a leading
  # separator in a @skip_files entry to imply that a directory is
  # "absolute" with respect to the project directory.
  #
  # Also directories need a trailing separator.
  project_relative_path = if File.directory?(path)
    File.join(
      File::SEPARATOR,
      absolute_path.relative_path_from(@project_root_path).to_s,
      File::SEPARATOR
    )
  else
    File.join(
      File::SEPARATOR,
      absolute_path.relative_path_from(@project_root_path).to_s
    )
  end
  files.match(project_relative_path)
end

def model_paths

def model_paths
  @model_paths ||= prioritize_concerns(find_paths("app/**/models"))
end

def prioritize_concerns paths

def prioritize_concerns paths
  paths.partition { |path| path.relative.include? "concerns" }.flatten
end

def reject_directories(paths)

def reject_directories(paths)
  paths.reject do |path|
    Brakeman.logger.spin
    File.directory?(path)
  end
end

def reject_global_excludes(paths)

def reject_global_excludes(paths)
  paths.reject do |path|
    relative_path = path.relative
    if @skip_vendor and relative_path.include? 'vendor/' and !in_engine_paths?(path) and !in_add_libs_paths?(path)
      true
    else
      match_path EXCLUDED_PATHS, path
    end
  end
end

def reject_skipped_files(paths)

def reject_skipped_files(paths)
  return paths unless @skip_files
  paths.reject do |path|
    Brakeman.logger.spin
    match_path @skip_files, path
  end
end

def relative_path(path)

Use AppTree#file_path(path).relative instead.
Should only be used by Brakeman::FilePath
def relative_path(path)
  pname = Pathname.new path
  if path and not path.empty? and pname.absolute?
    pname.relative_path_from(Pathname.new(self.root)).to_s
  else
    path
  end
end

def root_search_pattern

def root_search_pattern
  return @root_search_pattern if @root_search_pattern
  @root_search_pattern = search_pattern(@root)
end

def ruby_file_paths

def ruby_file_paths
  find_paths(".").uniq
end

def search_pattern(root_dir)

def search_pattern(root_dir)
  abs = @absolute_engine_paths.to_a.map { |path| path.gsub(/#{File::SEPARATOR}+$/, '') }
  rel = @relative_engine_paths.to_a.map { |path| path.gsub(/#{File::SEPARATOR}+$/, '') }
  roots = ([root_dir] + abs).join(",")
  rel_engines = (rel + [""]).join("/,")
  "{#{roots}}/{#{rel_engines}}"
end

def select_files(paths)

def select_files(paths)
  paths = select_only_files(paths)
  paths = reject_skipped_files(paths)
  paths = convert_to_file_paths(paths)
  paths = reject_global_excludes(paths)
  paths = reject_directories(paths)
  paths
end

def select_only_files(paths)

def select_only_files(paths)
  return paths unless @only_files
  paths.select do |path|
    Brakeman.logger.spin
    match_path @only_files, path
  end
end

def template_paths

def template_paths
  @template_paths ||= find_paths(".", "*.{#{VIEW_EXTENSIONS}}") +
    find_paths(".", "*.{erb,haml,slim}").reject { |path| File.basename(path).count(".") > 1 }
end

def top_directories_pattern

def top_directories_pattern
  top_dirs = convert_to_file_paths(Dir.glob(File.join(root_search_pattern, '*/')))
  top_dirs.reject! { |d| File.symlink?(d) or !File.directory?(d) }
  top_dirs = reject_global_excludes(top_dirs)
  top_dirs = reject_skipped_files(top_dirs)
  if top_dirs.empty?
    # Fall back to searching everything, otherwise the empty pattern
    # will start searching from the global root
    root_search_pattern
  else
    "{#{top_dirs.join(',')}}"
  end
end