Skip to content

Commit 6e5a5e9

Browse files
tanukiti1987drwl
andauthored
Fix NoMethodError when processing empty files (#232)
## Summary This PR fixes issue #182 where `annotaterb models --force` fails with a `NoMethodError: undefined method '+' for nil` when processing empty model files or files that contain no classes/modules. ## Problem Description When `annotaterb` encounters an empty file or a file with only comments/whitespace: 1. The file parser returns empty arrays for `parsed.starts` and `parsed.ends` 2. `parsed.ends.last` returns `nil` instead of a `[name, line_number]` tuple 3. The generator attempts to call `line_number_after + 1` where `line_number_after` is `nil` 4. This results in a `NoMethodError: undefined method '+' for nil` **Error Stack Trace:** ``` undefined method '+' for nil (NoMethodError) line_number_after + 1 ``` ## Root Cause The issue occurs in two methods within `AnnotatedFile::Generator`: 1. **`content_annotated_after`** - Tries to access `line_number_after + 1` without checking for `nil` 2. **`determine_annotation_position`** - Returns `nil` when `parsed.starts` is empty ## Solution ### Core Fixes **1. Enhanced `content_annotated_after` method:** - Added explicit `nil` check for `line_number_after` - Provides graceful fallback behavior for empty files - Appends annotations to the end of file content when no classes/modules are found **2. Enhanced `determine_annotation_position` method:** - Added safety check for empty `parsed.starts` array - Returns safe default `[nil, 0]` tuple for empty files - Maintains backward compatibility with existing logic ## Edge Cases Handled 1. **Completely empty files** (`""`) - Annotations added as file content 2. **Whitespace-only files** - Annotations placed appropriately 3. **Comment-only files** - Annotations placed before/after comments based on position 4. **Files with no classes/modules** - Safe fallback behavior ## Related Issues Fixes #182 ## Testing Instructions 1. Create an empty model file: `touch app/models/empty_model.rb` 2. Run: `annotaterb models --force` 3. Verify: No error occurs and annotation is added to the file 4. Test with files containing only comments 5. Run existing test suite: `bundle exec rspec` Co-authored-by: Andrew W. Lee <[email protected]>
1 parent 663fa08 commit 6e5a5e9

File tree

3 files changed

+246
-0
lines changed

3 files changed

+246
-0
lines changed

lib/annotate_rb/model_annotator/annotated_file/generator.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ def content_annotated_before(parsed, content_without_annotations, write_position
6363
# When nested_position is enabled, finds the most deeply nested class declaration
6464
# to place annotations directly above nested classes instead of at the file top.
6565
def determine_annotation_position(parsed)
66+
# Handle empty files where no classes/modules are found
67+
return [nil, 0] if parsed.starts.empty?
68+
6669
return parsed.starts.first unless @options[:nested_position]
6770

6871
class_entries = parsed.starts.select { |name, _line| parsed.type_map[name] == :class }
@@ -90,6 +93,17 @@ def determine_indentation(content_without_annotations, line_number_before)
9093
def content_annotated_after(parsed, content_without_annotations)
9194
_constant_name, line_number_after = parsed.ends.last
9295

96+
# Handle empty files where no classes/modules are found
97+
if line_number_after.nil?
98+
content_lines = content_without_annotations.lines
99+
# For empty files, append annotations at the end
100+
content_with_annotations_written_after = []
101+
content_with_annotations_written_after << content_lines
102+
content_with_annotations_written_after << $/ unless content_lines.empty?
103+
content_with_annotations_written_after << @new_wrapped_annotations.lines
104+
return content_with_annotations_written_after.join
105+
end
106+
93107
content_with_annotations_written_after = []
94108
content_with_annotations_written_after << content_without_annotations.lines[0..line_number_after]
95109
content_with_annotations_written_after << $/

spec/lib/annotate_rb/model_annotator/annotated_file/generator_spec.rb

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,5 +738,160 @@ module AnotherModule
738738
end
739739
end
740740
end
741+
742+
context "when file is empty" do
743+
let(:file_content) { "" }
744+
let(:new_annotations) do
745+
<<~ANNOTATIONS
746+
# == Schema Information
747+
#
748+
# Table name: users
749+
#
750+
# id :bigint not null, primary key
751+
#
752+
ANNOTATIONS
753+
end
754+
755+
context 'with position "before"' do
756+
let(:options) { AnnotateRb::Options.new({position_in_class: "before"}) }
757+
758+
let(:expected_content) do
759+
<<~CONTENT
760+
# == Schema Information
761+
#
762+
# Table name: users
763+
#
764+
# id :bigint not null, primary key
765+
#
766+
CONTENT
767+
end
768+
769+
it "adds annotations to empty file" do
770+
is_expected.to eq(expected_content)
771+
end
772+
end
773+
774+
context 'with position "after"' do
775+
let(:options) { AnnotateRb::Options.new({position_in_class: "after"}) }
776+
777+
let(:expected_content) do
778+
<<~CONTENT
779+
# == Schema Information
780+
#
781+
# Table name: users
782+
#
783+
# id :bigint not null, primary key
784+
#
785+
CONTENT
786+
end
787+
788+
it "adds annotations to empty file" do
789+
is_expected.to eq(expected_content)
790+
end
791+
end
792+
793+
context 'with position "top"' do
794+
let(:options) { AnnotateRb::Options.new({position_in_class: "top"}) }
795+
796+
let(:expected_content) do
797+
<<~CONTENT
798+
# == Schema Information
799+
#
800+
# Table name: users
801+
#
802+
# id :bigint not null, primary key
803+
#
804+
CONTENT
805+
end
806+
807+
it "adds annotations to empty file" do
808+
is_expected.to eq(expected_content)
809+
end
810+
end
811+
812+
context 'with position "bottom"' do
813+
let(:options) { AnnotateRb::Options.new({position_in_class: "bottom"}) }
814+
815+
let(:expected_content) do
816+
<<~CONTENT
817+
# == Schema Information
818+
#
819+
# Table name: users
820+
#
821+
# id :bigint not null, primary key
822+
#
823+
CONTENT
824+
end
825+
826+
it "adds annotations to empty file" do
827+
is_expected.to eq(expected_content)
828+
end
829+
end
830+
end
831+
832+
context "when file contains only whitespace and comments" do
833+
let(:file_content) do
834+
<<~FILE
835+
# Some random comment
836+
837+
# Another comment
838+
FILE
839+
end
840+
let(:new_annotations) do
841+
<<~ANNOTATIONS
842+
# == Schema Information
843+
#
844+
# Table name: users
845+
#
846+
# id :bigint not null, primary key
847+
#
848+
ANNOTATIONS
849+
end
850+
851+
context 'with position "before"' do
852+
let(:options) { AnnotateRb::Options.new({position_in_class: "before"}) }
853+
854+
let(:expected_content) do
855+
<<~CONTENT
856+
# == Schema Information
857+
#
858+
# Table name: users
859+
#
860+
# id :bigint not null, primary key
861+
#
862+
# Some random comment
863+
864+
# Another comment
865+
CONTENT
866+
end
867+
868+
it "adds annotations at the beginning of file with only comments" do
869+
is_expected.to eq(expected_content)
870+
end
871+
end
872+
873+
context 'with position "after"' do
874+
let(:options) { AnnotateRb::Options.new({position_in_class: "after"}) }
875+
876+
let(:expected_content) do
877+
<<~CONTENT
878+
# Some random comment
879+
880+
# Another comment
881+
882+
# == Schema Information
883+
#
884+
# Table name: users
885+
#
886+
# id :bigint not null, primary key
887+
#
888+
CONTENT
889+
end
890+
891+
it "adds annotations at the end of file with only comments" do
892+
is_expected.to eq(expected_content)
893+
end
894+
end
895+
end
741896
end
742897
end

spec/lib/annotate_rb/model_annotator/file_parser/custom_parser_spec.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,5 +321,82 @@ class User < ApplicationRecord
321321
check_it_parses_correctly
322322
end
323323
end
324+
325+
context "when file is completely empty" do
326+
let(:input) { "" }
327+
let(:expected_comments) { [] }
328+
let(:expected_starts) { [] }
329+
let(:expected_ends) { [] }
330+
331+
it "parses correctly and returns empty arrays" do
332+
check_it_parses_correctly
333+
end
334+
end
335+
336+
context "when file contains only whitespace" do
337+
let(:input) do
338+
<<~FILE
339+
340+
341+
342+
FILE
343+
end
344+
let(:expected_comments) { [] }
345+
let(:expected_starts) { [] }
346+
let(:expected_ends) { [] }
347+
348+
it "parses correctly and returns empty arrays" do
349+
check_it_parses_correctly
350+
end
351+
end
352+
353+
context "when file contains only comments" do
354+
let(:input) do
355+
<<~FILE
356+
# This is just a comment
357+
# Another comment
358+
359+
# Final comment
360+
FILE
361+
end
362+
let(:expected_comments) do
363+
[
364+
["# This is just a comment", 0],
365+
["# Another comment", 1],
366+
["# Final comment", 3]
367+
]
368+
end
369+
let(:expected_starts) { [] }
370+
let(:expected_ends) { [] }
371+
372+
it "parses comments but returns empty arrays for starts and ends" do
373+
check_it_parses_correctly
374+
end
375+
end
376+
377+
context "when file contains only block comments" do
378+
let(:input) do
379+
<<~FILE
380+
=begin
381+
This is a multi-line comment
382+
with no actual Ruby code
383+
=end
384+
FILE
385+
end
386+
let(:expected_comments) do
387+
[
388+
["=begin", 0],
389+
["This is a multi-line comment", 1],
390+
["with no actual Ruby code", 2],
391+
["=end", 3]
392+
]
393+
end
394+
let(:expected_starts) { [] }
395+
let(:expected_ends) { [] }
396+
397+
it "parses block comments but returns empty arrays for starts and ends" do
398+
check_it_parses_correctly
399+
end
400+
end
324401
end
325402
end

0 commit comments

Comments
 (0)