From 83b528abc3d0bf927cfa1041839f04cfe334d099 Mon Sep 17 00:00:00 2001
From: Nobuyoshi Nakada <nobu@ruby-lang.org>
Date: Sat, 16 Apr 2022 21:16:51 +0900
Subject: [PATCH] New line-oriented `.document` file format

Introduce the new format similar to the `.gitignore` file by starting
with the comment `rdoc.document: 1`.

- one pattern per line
- negative pattern starting with `!`
- escaping space and `#` by a backslash
---
 lib/rdoc/rdoc.rb            | 62 ++++++++++++++++++++++++++++++++++---
 test/rdoc/test_rdoc_rdoc.rb | 43 +++++++++++++++++++++++++
 2 files changed, 100 insertions(+), 5 deletions(-)

diff --git a/lib/rdoc/rdoc.rb b/lib/rdoc/rdoc.rb
index a7f9239b62..04a00cd347 100644
--- a/lib/rdoc/rdoc.rb
+++ b/lib/rdoc/rdoc.rb
@@ -245,15 +245,67 @@ def output_flag_file(op_dir)
   # The .document file contains a list of file and directory name patterns,
   # representing candidates for documentation. It may also contain comments
   # (starting with '#')
+  #
+  # If the first line is the comment starts with +rdoc.document:+
+  # (case-insensitive) followed by a version string, the file is
+  # parsed as per the version.  If a version is not written, it is
+  # defaulted to 0.
+  #
+  # version 0::
+  #
+  #   The file will be parsed as white-space separated glob patterns.
+  #
+  #   - A <tt>#</tt> in middle starts a comment.
+  #
+  #   - Multiple patterns can be in a single line.
+  #
+  #   - That means patterns cannot contain white-spaces and <tt>#</tt>
+  #     marks.
+  #
+  # version 1::
+  #
+  #   The file will be parsed as single glob pattern per each line.
+  #
+  #   - Only lines starting with <tt>#</tt> at the first colmun are
+  #     comments.  A <tt>#</tt> in middle is a part of the pattern.
+  #
+  #   - Patterns starting with <tt>#</tt> need to be prefixed with a
+  #     backslash (<tt>\\</tt>).
+  #
+  #   - Leading spaces are not stripped while trailing spaces which
+  #     are not escaped with a backslash are stripped.
+  #
+  #   - The pattern starting with <tt>!</tt> is a negative pattern,
+  #     which rejects matching files.
 
   def parse_dot_doc_file in_dir, filename
-    # read and strip comments
-    patterns = File.read(filename).gsub(/#.*/, '')
-
     result = {}
+    patterns = rejects = nil
+
+    content = File.read(filename)
+    version = content[/\A#+\s*rdoc\.document:\s*\K\S+/i]&.to_i || 0
+    if version >= 1
+      content.each_line(chomp: true) do |line|
+        next if line.start_with?("#") # skip comments
+        line.sub!(/(?<!\\)\s*$/, "") # rstrip unescaped trailing spaces
+        (line.sub!(/\A!/, "") ? (rejects ||= []) : (patterns ||= [])) << line
+      end
+    else
+      # read and strip comments
+      patterns = content.gsub(/#.*/, '').split(' ')
+    end
 
-    patterns.split(' ').each do |patt|
-      candidates = Dir.glob(File.join(in_dir, patt))
+    if patterns
+      patterns.each {|patt| patt.sub!(/\A\/+/, "")}
+      candidates = Dir.glob(patterns, base: in_dir)
+      if rejects
+        rejects.each {|patt| patt.sub!(/\A\/+/, "")}
+        flag = File::FNM_PATHNAME
+        candidates.delete_if do |name|
+          rejects.any? {|patt| File.fnmatch?(patt, name, flag)}
+        end
+      end
+      candidates.map! {|name| File.join(in_dir, name)}
       result.update normalized_file_list(candidates, false, @options.exclude)
     end
 
diff --git a/test/rdoc/test_rdoc_rdoc.rb b/test/rdoc/test_rdoc_rdoc.rb
index 9c94988ffd..ae7925a6ec 100644
--- a/test/rdoc/test_rdoc_rdoc.rb
+++ b/test/rdoc/test_rdoc_rdoc.rb
@@ -177,6 +177,49 @@ def test_normalized_file_list_with_dot_doc
     assert_equal expected_files, files
   end
 
+  def test_normalized_file_list_with_dot_doc_version_1
+    expected_files = []
+    files = temp_dir do |dir|
+      a = 'a.rb'
+      b = 'b.rb'
+      a_b = 'a.rb b.rb'
+      FileUtils.touch a
+      FileUtils.touch b
+      FileUtils.touch a_b
+
+      File.open('.document', 'w') do |f|
+        f.puts '# rdoc.document: 1'
+        f.puts a_b
+      end
+      expected_files << File.expand_path(a_b, dir)
+
+      @rdoc.normalized_file_list [dir]
+    end
+
+    assert_equal expected_files, files.keys
+  end
+
+  def test_normalized_file_list_with_dot_doc_negative_pattern
+    expected_files = []
+    files = temp_dir do |dir|
+      a = 'a.rb'
+      b = 'b.rb'
+      FileUtils.touch a
+      FileUtils.touch b
+
+      File.open('.document', 'w') do |f|
+        f.puts '# rdoc.document: 1'
+        f.puts '*.rb'
+        f.puts '!b.rb'
+      end
+      expected_files << File.expand_path(a, dir)
+
+      @rdoc.normalized_file_list [dir]
+    end
+
+    assert_equal expected_files, files.keys
+  end
+
   def test_normalized_file_list_with_dot_doc_overridden_by_exclude_option
     expected_files = []
     files = temp_dir do |dir|