Repository: standardrb/standard (Ruby)

Issue:
`standardrb --generate-todo` replaces any existing todo file with deltas only
@mrbiggred FYI,

I was introducing `standard-rails` to the project I'm trying to convert from plain rubocop (because @searls fixed standardrb/standard-rails/issues/7, tyvm), but when I ran `standardrb --generate-todo` it replaced the existing todo file with one that only contained ignores for the Rails cops, losing the previous ignores.

The fix was to remove .standard_todo.yml first, but I wonder if that should be documented or if it can be automated.


Discussion:
Definitely sounds like a boog.

Strong suspect it has something to do with using the `config` in its runner and then dumping out the resulting error file as a todo in [genrunner.rb](https://github.com/standardrb/standard/blob/main/lib/standard/runners/genignore.rb#L8-L15).

```ruby
# Run Rubocop to generate the files with errors into the temp file.
config.rubocop_options[:formatters] = [["json", temp_file.path]]
config.rubocop_options[:out] = temp_file.path
exit_code = Runners::Rubocop.new.call(config)
```

That `config` will have been configured normally, including with all the ignores of the [todo yaml file](https://github.com/standardrb/standard/blob/main/lib/standard/loads_yaml_config.rb#L10), meaning those won't be present so they'll be excluded in the newly generated file. 
@knutsenm thanks for bringing up the issue and finding a workaround of removing the existing the `.standard_todo.yml` file before regenerating the new todo file.  That will help other people who encounter this issue.

My initial guess as to the root cause is the same as @searls:

> That config will have been configured normally, including with all the ignores of the [todo yaml file](https://github.com/standardrb/standard/blob/main/lib/standard/loads_yaml_config.rb#L10), meaning those won't be present so they'll be excluded in the newly generated file.

I won't have time to investigate this issue till later this week but right off the top of my head a possible fix for the `--generate-todo` process to delete the `.standard_todo.yml`.

Let me know if you there is anything else that would make adding Standard to an existing project easier.
I was going to suggest the internal equivalent of `standardrb --todo /dev/null`, but then I noticed that

- that option, and `--config`, have both been dropped from the README (although they're still in the code), and
- when I try it, I get an error: it's being passed through to rubocop, which doesn't recognize it; a separate bug?

The `--config` option does seem to work, perhaps because Standard::MergesSettings#without_banned strips it. Maybe it ought to do the same for `--yaml`? Or perhaps ResolvesYamlOption ought to strip what it finds from argv; I'm not sure.



Patch:
diff --git a/lib/standard/builds_config.rb b/lib/standard/builds_config.rb
index df221d31..352e7178 100644
--- a/lib/standard/builds_config.rb
+++ b/lib/standard/builds_config.rb
@@ -15,7 +15,13 @@ def initialize
 
     def call(argv, search_path = Dir.pwd)
       standard_yaml_path = @resolves_yaml_option.call(argv, search_path, "--config", ".standard.yml")
-      todo_yaml_path = @resolves_yaml_option.call(argv, search_path, "--todo", ".standard_todo.yml")
+
+      # Don't load the existing todo file when generating a new todo file.  Otherwise the
+      # new todo file won't have the ignore rules in the existing file.
+      todo_yaml_path = unless argv.include?("--generate-todo")
+        @resolves_yaml_option.call(argv, search_path, "--todo", ".standard_todo.yml")
+      end
+
       standard_config = @loads_yaml_config.call(standard_yaml_path, todo_yaml_path)
 
       settings = @merges_settings.call(argv, standard_config)


Test patch:
diff --git a/test/fixture/config/t/.standard.yml b/test/fixture/config/t/.standard.yml
deleted file mode 100644
index e2784730..00000000
--- a/test/fixture/config/t/.standard.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-ignore:
-  - none_todo_path/**/*
-  - none_todo_file.rb
diff --git a/test/fixture/config/t/.standard_todo.yml b/test/fixture/config/t/.standard_todo.yml
deleted file mode 100644
index bb622faa..00000000
--- a/test/fixture/config/t/.standard_todo.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-ignore:
-  - todo_file_one.rb:
-      - Lint/AssignmentInCondition
-  - todo_file_two.rb
diff --git a/test/fixture/config/u/.standard_todo.yml b/test/fixture/config/u/.standard_todo.yml
index babc8424..d765abb1 100644
--- a/test/fixture/config/u/.standard_todo.yml
+++ b/test/fixture/config/u/.standard_todo.yml
@@ -1,3 +1,3 @@
 ignore:
   - todo_file_one.rb
-  - todo_file_two.rb
+  - todo_file_two.rb
\ No newline at end of file
diff --git a/test/fixture/standardrb/generate_todo/.standard.yml b/test/fixture/standardrb/generate_todo/.standard.yml
new file mode 100644
index 00000000..a347c552
--- /dev/null
+++ b/test/fixture/standardrb/generate_todo/.standard.yml
@@ -0,0 +1,1 @@
+ruby_version: 2.6
\ No newline at end of file
diff --git a/test/fixture/standardrb/generate_todo/.standard_todo_expected.yml b/test/fixture/standardrb/generate_todo/.standard_todo_expected.yml
new file mode 100644
index 00000000..27fb6fc6
--- /dev/null
+++ b/test/fixture/standardrb/generate_todo/.standard_todo_expected.yml
@@ -0,0 +1,6 @@
+# Auto generated files with errors to ignore.
+# Remove from this list as you refactor files.
+---
+ignore:
+- useless_assignment.rb:
+  - Lint/UselessAssignment
diff --git a/test/fixture/standardrb/generate_todo/useless_assignment.rb b/test/fixture/standardrb/generate_todo/useless_assignment.rb
new file mode 100644
index 00000000..850fdbd9
--- /dev/null
+++ b/test/fixture/standardrb/generate_todo/useless_assignment.rb
@@ -0,0 +1,2 @@
+useless_assignment = "LOL"
+useless_assignment2 = "LOL"
diff --git a/test/fixture/standardrb/generate_todo_existing/.standard.yml b/test/fixture/standardrb/generate_todo_existing/.standard.yml
new file mode 100644
index 00000000..a347c552
--- /dev/null
+++ b/test/fixture/standardrb/generate_todo_existing/.standard.yml
@@ -0,0 +1,1 @@
+ruby_version: 2.6
\ No newline at end of file
diff --git a/test/fixture/standardrb/generate_todo_existing/.standard_todo.yml b/test/fixture/standardrb/generate_todo_existing/.standard_todo.yml
new file mode 100644
index 00000000..27fb6fc6
--- /dev/null
+++ b/test/fixture/standardrb/generate_todo_existing/.standard_todo.yml
@@ -0,0 +1,6 @@
+# Auto generated files with errors to ignore.
+# Remove from this list as you refactor files.
+---
+ignore:
+- useless_assignment.rb:
+  - Lint/UselessAssignment
diff --git a/test/fixture/standardrb/generate_todo_existing/.standard_todo_expected.yml b/test/fixture/standardrb/generate_todo_existing/.standard_todo_expected.yml
new file mode 100644
index 00000000..87531295
--- /dev/null
+++ b/test/fixture/standardrb/generate_todo_existing/.standard_todo_expected.yml
@@ -0,0 +1,8 @@
+# Auto generated files with errors to ignore.
+# Remove from this list as you refactor files.
+---
+ignore:
+- assignment_in_condition.rb:
+  - Lint/AssignmentInCondition
+- useless_assignment.rb:
+  - Lint/UselessAssignment
diff --git a/test/fixture/standardrb/generate_todo_existing/assignment_in_condition.rb b/test/fixture/standardrb/generate_todo_existing/assignment_in_condition.rb
new file mode 100644
index 00000000..fd335b5a
--- /dev/null
+++ b/test/fixture/standardrb/generate_todo_existing/assignment_in_condition.rb
@@ -0,0 +1,9 @@
+class ErrorsOne
+  def stuff(id)
+    if bar = Bar.find(id)
+      bar.nice!
+    else
+      false
+    end
+  end
+end
diff --git a/test/fixture/standardrb/generate_todo_existing/useless_assignment.rb b/test/fixture/standardrb/generate_todo_existing/useless_assignment.rb
new file mode 100644
index 00000000..850fdbd9
--- /dev/null
+++ b/test/fixture/standardrb/generate_todo_existing/useless_assignment.rb
@@ -0,0 +1,2 @@
+useless_assignment = "LOL"
+useless_assignment2 = "LOL"
diff --git a/test/standard/builds_config_test.rb b/test/standard/builds_config_test.rb
index 4b952dba..09ce1841 100644
--- a/test/standard/builds_config_test.rb
+++ b/test/standard/builds_config_test.rb
@@ -78,6 +78,12 @@ def test_specified_standard_yaml_raises
     assert_match(/Configuration file ".*fake\.file" not found/, err.message)
   end
 
+  def test_todo_file_not_loaded_when_generating_todo_file
+    result = @subject.call(["--generate-todo"], path("test/fixture/config/t"))
+
+    assert_equal DEFAULT_OPTIONS, result.rubocop_options
+  end
+
   def test_todo_merged
     result = @subject.call([], path("test/fixture/config/u"))
 
@@ -93,19 +99,4 @@ def test_todo_merged
     refute_includes resulting_options_config["Metric/LineLength"]["Exclude"], path("test/fixture/config/u/thing.rb")
     assert_includes resulting_options_config["Metric/LineLength"]["Exclude"], path("test/fixture/config/u/stuff.rb")
   end
-
-  def test_todo_with_offenses_merged
-    result = @subject.call([], path("test/fixture/config/t"))
-
-    assert_equal DEFAULT_OPTIONS.merge(
-      todo_file: path("test/fixture/config/t/.standard_todo.yml"),
-      todo_ignore_files: %w[todo_file_one.rb todo_file_two.rb]
-    ), result.rubocop_options
-
-    resulting_options_config = result.rubocop_config_store.for("").to_h
-    assert_includes resulting_options_config["AllCops"]["Exclude"], path("test/fixture/config/t/none_todo_path/**/*")
-    assert_includes resulting_options_config["AllCops"]["Exclude"], path("test/fixture/config/t/none_todo_file.rb")
-    assert_includes resulting_options_config["AllCops"]["Exclude"], path("test/fixture/config/t/todo_file_two.rb")
-    assert_includes resulting_options_config["Lint/AssignmentInCondition"]["Exclude"], path("test/fixture/config/t/todo_file_one.rb")
-  end
 end
diff --git a/test/standardrb_test.rb b/test/standardrb_test.rb
index 01244d3a..3dc4a17e 100644
--- a/test/standardrb_test.rb
+++ b/test/standardrb_test.rb
@@ -2,6 +2,14 @@
 require "open3"
 
 class StandardrbTest < UnitTest
+  FIXTURE_ROOT_FOLDER = "test/fixture"
+  TEST_ROOT_FOLDER = "tmp/tests/standardrb"
+
+  # Initialize the test folder once for all the tests
+  # in this file.
+  FileUtils.rm_rf(TEST_ROOT_FOLDER)
+  FileUtils.mkdir_p(TEST_ROOT_FOLDER)
+
   def test_project_a_failure_output
     stdout, status = run_standardrb("test/fixture/project/a")
 
@@ -15,6 +23,14 @@ def test_project_a_failure_output
     MSG
   end
 
+  def test_generate_todo
+    assert_generate_todo("standardrb/generate_todo", "generate_todo", %w[--generate-todo --config ./.standard.yml])
+  end
+
+  def test_generate_todo_existing
+    assert_generate_todo("standardrb/generate_todo_existing", "generate_todo_existing", %w[--generate-todo --config ./.standard.yml])
+  end
+
   def test_project_a_forwards_rubocop_options
     stdout, status = run_standardrb("test/fixture/project/a", %w[--only Style/TrivialAccessors])
 
@@ -77,4 +93,21 @@ def run_standardrb(cwd, args = [])
   def assert_same_lines(expected, actual)
     assert_equal expected.split("\n").sort, actual.split("\n").sort
   end
+
+  def assert_generate_todo(fixture_subfolder, test_subfolder, arguments)
+    fixture_folder = File.join(FIXTURE_ROOT_FOLDER, fixture_subfolder)
+    test_folder = File.join(TEST_ROOT_FOLDER, test_subfolder)
+
+    FileUtils.cp_r(File.join(fixture_folder, "."), test_folder)
+
+    stdout, status = run_standardrb(test_folder, arguments)
+
+    # The generate todo returns the Rubocop result which is non-zero
+    # if any linting errors where found.
+    refute status.success?
+    assert stdout.empty?
+
+    assert File.exist?(File.join(test_folder, ".standard_todo.yml"))
+    assert_equal File.read(File.join(test_folder, ".standard_todo_expected.yml")), File.read(File.join(test_folder, ".standard_todo.yml"))
+  end
 end