annotaterb 4.22.0 → 4.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +83 -0
- data/README.md +156 -4
- data/VERSION +1 -1
- data/lib/annotate_rb/helper.rb +24 -1
- data/lib/annotate_rb/model_annotator/annotated_file/generator.rb +34 -12
- data/lib/annotate_rb/model_annotator/annotated_file/updater.rb +14 -1
- data/lib/annotate_rb/model_annotator/annotation/annotation_builder.rb +9 -1
- data/lib/annotate_rb/model_annotator/annotation_decider.rb +3 -0
- data/lib/annotate_rb/model_annotator/annotation_diff_generator.rb +9 -4
- data/lib/annotate_rb/model_annotator/column_annotation/attributes_builder.rb +7 -0
- data/lib/annotate_rb/model_annotator/column_annotation/column_component.rb +2 -2
- data/lib/annotate_rb/model_annotator/column_annotation/column_wrapper.rb +4 -0
- data/lib/annotate_rb/model_annotator/enum_annotation/annotation.rb +40 -0
- data/lib/annotate_rb/model_annotator/enum_annotation/annotation_builder.rb +38 -0
- data/lib/annotate_rb/model_annotator/enum_annotation/enum_component.rb +27 -0
- data/lib/annotate_rb/model_annotator/enum_annotation.rb +11 -0
- data/lib/annotate_rb/model_annotator/exclusion_constraint_annotation/annotation.rb +40 -0
- data/lib/annotate_rb/model_annotator/exclusion_constraint_annotation/annotation_builder.rb +38 -0
- data/lib/annotate_rb/model_annotator/exclusion_constraint_annotation/exclusion_constraint_component.rb +27 -0
- data/lib/annotate_rb/model_annotator/exclusion_constraint_annotation.rb +11 -0
- data/lib/annotate_rb/model_annotator/file_parser/annotation_finder.rb +75 -4
- data/lib/annotate_rb/model_annotator/file_parser/annotation_target.rb +31 -0
- data/lib/annotate_rb/model_annotator/file_parser/custom_parser.rb +11 -4
- data/lib/annotate_rb/model_annotator/file_parser/magic_comment.rb +32 -0
- data/lib/annotate_rb/model_annotator/file_parser/parsed_file.rb +5 -6
- data/lib/annotate_rb/model_annotator/file_parser/yml_parser.rb +43 -4
- data/lib/annotate_rb/model_annotator/file_parser.rb +2 -0
- data/lib/annotate_rb/model_annotator/foreign_key_annotation/foreign_key_component_builder.rb +6 -0
- data/lib/annotate_rb/model_annotator/index_annotation/annotation_builder.rb +29 -0
- data/lib/annotate_rb/model_annotator/index_annotation/index_component.rb +26 -4
- data/lib/annotate_rb/model_annotator/model_wrapper.rb +58 -2
- data/lib/annotate_rb/model_annotator/project_annotator.rb +2 -1
- data/lib/annotate_rb/model_annotator/single_file_annotation_remover.rb +1 -1
- data/lib/annotate_rb/model_annotator/single_file_annotator.rb +6 -6
- data/lib/annotate_rb/model_annotator/single_file_annotator_instruction.rb +3 -2
- data/lib/annotate_rb/model_annotator/unique_constraint_annotation/annotation.rb +40 -0
- data/lib/annotate_rb/model_annotator/unique_constraint_annotation/annotation_builder.rb +37 -0
- data/lib/annotate_rb/model_annotator/unique_constraint_annotation/unique_constraint_component.rb +27 -0
- data/lib/annotate_rb/model_annotator/unique_constraint_annotation.rb +11 -0
- data/lib/annotate_rb/model_annotator/zeitwerk_class_getter.rb +1 -1
- data/lib/annotate_rb/model_annotator.rb +3 -0
- data/lib/annotate_rb/options.rb +12 -2
- data/lib/annotate_rb/parser.rb +23 -6
- data/lib/annotate_rb/route_annotator/base_processor.rb +1 -1
- data/lib/annotate_rb/route_annotator/helper.rb +1 -1
- data/lib/annotate_rb/runner.rb +12 -6
- data/lib/annotate_rb/tasks/annotate_models_migrate.rake +8 -3
- data/lib/generators/annotate_rb/update_config/update_config_generator.rb +4 -1
- metadata +16 -2
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnnotateRb
|
|
4
|
+
module ModelAnnotator
|
|
5
|
+
module EnumAnnotation
|
|
6
|
+
class EnumComponent < Components::Base
|
|
7
|
+
attr_reader :name, :values, :max_size
|
|
8
|
+
|
|
9
|
+
def initialize(name, values, max_size)
|
|
10
|
+
@name = name
|
|
11
|
+
@values = values
|
|
12
|
+
@max_size = max_size
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_default
|
|
16
|
+
# standard:disable Lint/FormatParameterMismatch
|
|
17
|
+
sprintf("# %-#{max_size}.#{max_size}s %s", name, values.join(", ")).rstrip
|
|
18
|
+
# standard:enable Lint/FormatParameterMismatch
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_markdown
|
|
22
|
+
sprintf("# * `%s`: `%s`", name, values.join(", "))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnnotateRb
|
|
4
|
+
module ModelAnnotator
|
|
5
|
+
module EnumAnnotation
|
|
6
|
+
autoload :AnnotationBuilder, "annotate_rb/model_annotator/enum_annotation/annotation_builder"
|
|
7
|
+
autoload :Annotation, "annotate_rb/model_annotator/enum_annotation/annotation"
|
|
8
|
+
autoload :EnumComponent, "annotate_rb/model_annotator/enum_annotation/enum_component"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnnotateRb
|
|
4
|
+
module ModelAnnotator
|
|
5
|
+
module ExclusionConstraintAnnotation
|
|
6
|
+
class Annotation
|
|
7
|
+
HEADER_TEXT = "Exclusion Constraints"
|
|
8
|
+
|
|
9
|
+
def initialize(constraints)
|
|
10
|
+
@constraints = constraints
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def body
|
|
14
|
+
[
|
|
15
|
+
Components::BlankCommentLine.new,
|
|
16
|
+
Components::Header.new(HEADER_TEXT),
|
|
17
|
+
Components::BlankCommentLine.new,
|
|
18
|
+
*@constraints
|
|
19
|
+
]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_markdown
|
|
23
|
+
body.map(&:to_markdown).join("\n")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_rdoc
|
|
27
|
+
body.map(&:to_rdoc).join("\n")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_yard
|
|
31
|
+
body.map(&:to_yard).join("\n")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_default
|
|
35
|
+
body.map(&:to_default).join("\n")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnnotateRb
|
|
4
|
+
module ModelAnnotator
|
|
5
|
+
module ExclusionConstraintAnnotation
|
|
6
|
+
class AnnotationBuilder
|
|
7
|
+
def initialize(model, options)
|
|
8
|
+
@model = model
|
|
9
|
+
@options = options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build
|
|
13
|
+
return Components::NilComponent.new if !@options[:show_exclusion_constraints]
|
|
14
|
+
return Components::NilComponent.new unless @model.connection.respond_to?(:supports_exclusion_constraints?) &&
|
|
15
|
+
@model.connection.supports_exclusion_constraints? && @model.connection.respond_to?(:exclusion_constraints)
|
|
16
|
+
|
|
17
|
+
exclusion_constraints = @model.connection.exclusion_constraints(@model.table_name)
|
|
18
|
+
return Components::NilComponent.new if exclusion_constraints.empty?
|
|
19
|
+
|
|
20
|
+
max_size = exclusion_constraints.map { |exclusion_constraint| exclusion_constraint.name.size }.max + 1
|
|
21
|
+
|
|
22
|
+
constraints = exclusion_constraints.sort_by(&:name).map do |exclusion_constraint|
|
|
23
|
+
details = "(#{exclusion_constraint.expression})"
|
|
24
|
+
details += " USING #{exclusion_constraint.using}" if exclusion_constraint.using
|
|
25
|
+
details += " WHERE (#{exclusion_constraint.where})" if exclusion_constraint.where
|
|
26
|
+
if exclusion_constraint.deferrable
|
|
27
|
+
details += " DEFERRABLE INITIALLY #{exclusion_constraint.deferrable.to_s.upcase}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
ExclusionConstraintComponent.new(exclusion_constraint.name, details, max_size)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
_annotation = Annotation.new(constraints)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnnotateRb
|
|
4
|
+
module ModelAnnotator
|
|
5
|
+
module ExclusionConstraintAnnotation
|
|
6
|
+
class ExclusionConstraintComponent < Components::Base
|
|
7
|
+
attr_reader :name, :details, :max_size
|
|
8
|
+
|
|
9
|
+
def initialize(name, details, max_size)
|
|
10
|
+
@name = name
|
|
11
|
+
@details = details
|
|
12
|
+
@max_size = max_size
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_default
|
|
16
|
+
# standard:disable Lint/FormatParameterMismatch
|
|
17
|
+
sprintf("# %-#{max_size}.#{max_size}s %s", name, details).rstrip
|
|
18
|
+
# standard:enable Lint/FormatParameterMismatch
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_markdown
|
|
22
|
+
sprintf("# * `%s`: `%s`", name, details)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnnotateRb
|
|
4
|
+
module ModelAnnotator
|
|
5
|
+
module ExclusionConstraintAnnotation
|
|
6
|
+
autoload :AnnotationBuilder, "annotate_rb/model_annotator/exclusion_constraint_annotation/annotation_builder"
|
|
7
|
+
autoload :Annotation, "annotate_rb/model_annotator/exclusion_constraint_annotation/annotation"
|
|
8
|
+
autoload :ExclusionConstraintComponent, "annotate_rb/model_annotator/exclusion_constraint_annotation/exclusion_constraint_component"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -8,6 +8,30 @@ module AnnotateRb
|
|
|
8
8
|
COMPAT_PREFIX_MD = "## Schema Info"
|
|
9
9
|
DEFAULT_ANNOTATION_ENDING = "#"
|
|
10
10
|
|
|
11
|
+
SCHEMA_HEADER_PREFIXES = [
|
|
12
|
+
COMPAT_PREFIX,
|
|
13
|
+
COMPAT_PREFIX_MD,
|
|
14
|
+
"Table name:",
|
|
15
|
+
"Database name:",
|
|
16
|
+
"Schema version:"
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
SCHEMA_HEADER_EXACT = [
|
|
20
|
+
IndexAnnotation::Annotation::HEADER_TEXT,
|
|
21
|
+
ForeignKeyAnnotation::Annotation::HEADER_TEXT,
|
|
22
|
+
CheckConstraintAnnotation::Annotation::HEADER_TEXT,
|
|
23
|
+
UniqueConstraintAnnotation::Annotation::HEADER_TEXT,
|
|
24
|
+
ExclusionConstraintAnnotation::Annotation::HEADER_TEXT,
|
|
25
|
+
EnumAnnotation::Annotation::HEADER_TEXT
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
# `format_markdown: true` renders each of the section headers above as
|
|
29
|
+
# "### <name>" instead of "<name>", so SCHEMA_HEADER_EXACT can't match it directly.
|
|
30
|
+
# There's no HEADER_TEXT constant for the columns table itself,
|
|
31
|
+
# so "Columns" is listed explicitly here.
|
|
32
|
+
MARKDOWN_HEADER_PREFIX = "### "
|
|
33
|
+
MARKDOWN_SECTION_HEADERS = (SCHEMA_HEADER_EXACT + ["Columns"]).freeze
|
|
34
|
+
|
|
11
35
|
class MalformedAnnotation < StandardError; end
|
|
12
36
|
|
|
13
37
|
class NoAnnotationFound < StandardError; end
|
|
@@ -70,10 +94,7 @@ module AnnotateRb
|
|
|
70
94
|
end
|
|
71
95
|
end
|
|
72
96
|
else
|
|
73
|
-
|
|
74
|
-
while ending > start && comments[ending].first != DEFAULT_ANNOTATION_ENDING
|
|
75
|
-
ending -= 1
|
|
76
|
-
end
|
|
97
|
+
ending = walk_forward_through_schema(comments, start, ending)
|
|
77
98
|
end
|
|
78
99
|
|
|
79
100
|
# We want .last because we want the line indexes
|
|
@@ -95,6 +116,56 @@ module AnnotateRb
|
|
|
95
116
|
def annotated?
|
|
96
117
|
@annotation_start && @annotation_end
|
|
97
118
|
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def walk_forward_through_schema(comments, start, block_end)
|
|
123
|
+
ending = start
|
|
124
|
+
while ending < block_end
|
|
125
|
+
break unless schema_like?(comments[ending + 1].first)
|
|
126
|
+
ending += 1
|
|
127
|
+
end
|
|
128
|
+
ending
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# A line is part of the schema block if it matches one of the known
|
|
132
|
+
# header prefixes/exact texts, or if it looks like a tabular/list row
|
|
133
|
+
# of the annotation body:
|
|
134
|
+
#
|
|
135
|
+
# - Default/plain format: rows have >= 2 leading spaces after `#`
|
|
136
|
+
# (e.g. "# id :integer not null"); prose has at most one.
|
|
137
|
+
# - Markdown format (`format_markdown: true`) instead uses a single
|
|
138
|
+
# leading space after `#` for everything, so it needs its own
|
|
139
|
+
# detection: "### <name>" section headers (matched against
|
|
140
|
+
# MARKDOWN_SECTION_HEADERS with the prefix stripped), "|"-delimited
|
|
141
|
+
# table rows (header/divider/data), and "* `...`" bulleted list
|
|
142
|
+
# items for indexes/foreign keys/enums.
|
|
143
|
+
def schema_like?(comment)
|
|
144
|
+
return true if comment == DEFAULT_ANNOTATION_ENDING
|
|
145
|
+
|
|
146
|
+
text = comment.sub(/\A#\s?/, "")
|
|
147
|
+
return false if text.empty?
|
|
148
|
+
|
|
149
|
+
return true if SCHEMA_HEADER_PREFIXES.any? { |p| text.start_with?(p) }
|
|
150
|
+
return true if SCHEMA_HEADER_EXACT.include?(text)
|
|
151
|
+
|
|
152
|
+
# Markdown section headers, e.g. "### Columns", "### Indexes".
|
|
153
|
+
if text.start_with?(MARKDOWN_HEADER_PREFIX)
|
|
154
|
+
return MARKDOWN_SECTION_HEADERS.include?(text.delete_prefix(MARKDOWN_HEADER_PREFIX))
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Requires at least two | separators to identify Markdown tables.
|
|
158
|
+
# This prevents normal user comments containing a single | from being mistaken for a table.
|
|
159
|
+
return true if text.count("|") >= 2
|
|
160
|
+
|
|
161
|
+
# Markdown bulleted list items used for foreign keys/indexes/enums,
|
|
162
|
+
# e.g. "* `index_name`:" or the nested " * **`column`**". The
|
|
163
|
+
# backtick right after the bullet marker distinguishes these from
|
|
164
|
+
# an unrelated user comment that starts with a plain "* ".
|
|
165
|
+
return true if text.match?(/\A\s*\*\s+\*{0,2}`/)
|
|
166
|
+
|
|
167
|
+
comment.match?(/\A#\s{2,}\S/)
|
|
168
|
+
end
|
|
98
169
|
end
|
|
99
170
|
end
|
|
100
171
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnnotateRb
|
|
4
|
+
module ModelAnnotator
|
|
5
|
+
module FileParser
|
|
6
|
+
# When `model_class_name` is given, matches that class declaration directly
|
|
7
|
+
# so inner classes inside the model body cannot be mistaken for the target.
|
|
8
|
+
module AnnotationTarget
|
|
9
|
+
SKIP_NAMES = %w[require require_relative load].freeze
|
|
10
|
+
|
|
11
|
+
def self.find(parser, options, model_class_name: nil)
|
|
12
|
+
starts = parser.starts.reject { |entry| SKIP_NAMES.include?(entry.first) }
|
|
13
|
+
return nil if starts.empty?
|
|
14
|
+
|
|
15
|
+
return starts.first unless options[:nested_position] && parser.respond_to?(:type_map)
|
|
16
|
+
|
|
17
|
+
if model_class_name && parser.type_map[model_class_name] == :class
|
|
18
|
+
match = starts.find { |name, _line| name == model_class_name }
|
|
19
|
+
return match if match
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class_entries = starts
|
|
23
|
+
.select { |name, _line| parser.type_map[name] == :class }
|
|
24
|
+
.uniq { |name, _line| name }
|
|
25
|
+
|
|
26
|
+
class_entries.last || starts.first
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -50,16 +50,23 @@ module AnnotateRb
|
|
|
50
50
|
}
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
def on_const(const)
|
|
54
|
+
@_last_const_lineno = lineno
|
|
55
|
+
super
|
|
56
|
+
end
|
|
57
|
+
|
|
53
58
|
def on_const_ref(const)
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
declaration_lineno = @_last_const_lineno || lineno
|
|
60
|
+
add_event(__method__, const, declaration_lineno)
|
|
61
|
+
@block_starts << [const, declaration_lineno]
|
|
56
62
|
super
|
|
57
63
|
end
|
|
58
64
|
|
|
59
65
|
# Used for `class Foo::User`
|
|
60
66
|
def on_const_path_ref(_left, const)
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
declaration_lineno = @_last_const_lineno || lineno
|
|
68
|
+
add_event(__method__, const, declaration_lineno)
|
|
69
|
+
@block_starts << [const, declaration_lineno]
|
|
63
70
|
super
|
|
64
71
|
end
|
|
65
72
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnnotateRb
|
|
4
|
+
module ModelAnnotator
|
|
5
|
+
module FileParser
|
|
6
|
+
module MagicComment
|
|
7
|
+
DIRECTIVE_KEYS = %w[
|
|
8
|
+
encoding
|
|
9
|
+
coding
|
|
10
|
+
frozen_string_literal
|
|
11
|
+
warn_indent
|
|
12
|
+
shareable_constant_value
|
|
13
|
+
typed
|
|
14
|
+
rbs_inline
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
SIMPLE = /\A\s*#\s*(?<key>[A-Za-z][A-Za-z0-9_-]*)\s*:\s*\S/
|
|
18
|
+
EMACS = /\A\s*#\s*-\*-.*-\*-\s*\z/
|
|
19
|
+
VIM = /\A\s*#\s*vim:\s/
|
|
20
|
+
|
|
21
|
+
def self.match?(line)
|
|
22
|
+
if (m = SIMPLE.match(line))
|
|
23
|
+
key = m[:key].downcase.tr("-", "_")
|
|
24
|
+
return true if DIRECTIVE_KEYS.include?(key)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
EMACS.match?(line) || VIM.match?(line)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -6,12 +6,13 @@ module AnnotateRb
|
|
|
6
6
|
class ParsedFile
|
|
7
7
|
SKIP_ANNOTATION_STRING = "# -*- SkipSchemaAnnotations"
|
|
8
8
|
|
|
9
|
-
def initialize(file_content, new_annotations, parser_klass, options)
|
|
9
|
+
def initialize(file_content, new_annotations, parser_klass, options, model_class_name: nil)
|
|
10
10
|
@file_content = file_content
|
|
11
11
|
@file_lines = @file_content.lines
|
|
12
12
|
@new_annotations = new_annotations
|
|
13
13
|
@parser_klass = parser_klass
|
|
14
14
|
@options = options
|
|
15
|
+
@model_class_name = model_class_name
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def parse
|
|
@@ -44,7 +45,7 @@ module AnnotateRb
|
|
|
44
45
|
annotation_start = @finder.annotation_start
|
|
45
46
|
annotation_end = @finder.annotation_end
|
|
46
47
|
|
|
47
|
-
if @file_lines[annotation_start - 1]&.strip&.empty?
|
|
48
|
+
if annotation_start > 0 && @file_lines[annotation_start - 1]&.strip&.empty?
|
|
48
49
|
annotation_start -= 1
|
|
49
50
|
has_leading_whitespace = true
|
|
50
51
|
end
|
|
@@ -64,10 +65,8 @@ module AnnotateRb
|
|
|
64
65
|
annotation_position = nil
|
|
65
66
|
|
|
66
67
|
if has_annotations
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# If the file does not have any class or module declaration then const_declaration can be nil
|
|
70
|
-
_const, line_number = const_declaration
|
|
68
|
+
target = AnnotationTarget.find(@file_parser, @options, model_class_name: @model_class_name)
|
|
69
|
+
_const, line_number = target if target
|
|
71
70
|
|
|
72
71
|
if line_number
|
|
73
72
|
annotation_position = if @finder.annotation_start < line_number
|
|
@@ -43,10 +43,20 @@ module AnnotateRb
|
|
|
43
43
|
begin
|
|
44
44
|
parser.parse(@input)
|
|
45
45
|
rescue Psych::SyntaxError => _e
|
|
46
|
-
# "Dynamic fixtures with ERB" exist in Rails
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
# "Dynamic fixtures with ERB" exist in Rails and cause Psych.parser to error.
|
|
47
|
+
#
|
|
48
|
+
# We deliberately do not evaluate the ERB and read line numbers off the
|
|
49
|
+
# result: evaluating runs arbitrary code, and the line numbers from the
|
|
50
|
+
# evaluated output do not map back to the original file (ERB tags spanning
|
|
51
|
+
# multiple lines shift the offsets), which would place annotations inside
|
|
52
|
+
# an ERB tag. Instead we derive the content bounds straight from the
|
|
53
|
+
# original lines so annotations land around the ERB body.
|
|
54
|
+
#
|
|
55
|
+
# Only fall back for actual ERB fixtures. Genuinely malformed YAML
|
|
56
|
+
# (no ERB tags) should keep surfacing the parse error rather than
|
|
57
|
+
# being silently annotated.
|
|
58
|
+
raise unless erb_fixture?
|
|
59
|
+
return record_erb_positions
|
|
50
60
|
end
|
|
51
61
|
|
|
52
62
|
stream = parser.handler.root
|
|
@@ -68,6 +78,35 @@ module AnnotateRb
|
|
|
68
78
|
@ends << [nil, stream.end_line]
|
|
69
79
|
end
|
|
70
80
|
end
|
|
81
|
+
|
|
82
|
+
# Locates the content bounds of an ERB fixture directly from the original
|
|
83
|
+
# lines, treating the ERB/YAML body as the doc. The start is the first
|
|
84
|
+
# non-blank, non-comment line so annotations are written above the ERB
|
|
85
|
+
# block (and after any leading comments), never inside a tag.
|
|
86
|
+
def record_erb_positions
|
|
87
|
+
lines = @input.split($/)
|
|
88
|
+
content_start = lines.index { |line| content_line?(line) }
|
|
89
|
+
|
|
90
|
+
if content_start.nil?
|
|
91
|
+
@starts << [nil, 0]
|
|
92
|
+
@ends << [nil, 0]
|
|
93
|
+
else
|
|
94
|
+
content_end = lines.rindex { |line| content_line?(line) }
|
|
95
|
+
@starts << [nil, content_start]
|
|
96
|
+
@ends << [nil, content_end + 1]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def content_line?(line)
|
|
101
|
+
stripped = line.strip
|
|
102
|
+
!stripped.empty? && !stripped.start_with?("#")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# True when the input contains an ERB tag, i.e. it is a dynamic fixture
|
|
106
|
+
# rather than plain (possibly malformed) YAML.
|
|
107
|
+
def erb_fixture?
|
|
108
|
+
@input.match?(/<%.*?%>/m)
|
|
109
|
+
end
|
|
71
110
|
end
|
|
72
111
|
end
|
|
73
112
|
end
|
|
@@ -4,7 +4,9 @@ module AnnotateRb
|
|
|
4
4
|
module ModelAnnotator
|
|
5
5
|
module FileParser
|
|
6
6
|
autoload :AnnotationFinder, "annotate_rb/model_annotator/file_parser/annotation_finder"
|
|
7
|
+
autoload :AnnotationTarget, "annotate_rb/model_annotator/file_parser/annotation_target"
|
|
7
8
|
autoload :CustomParser, "annotate_rb/model_annotator/file_parser/custom_parser"
|
|
9
|
+
autoload :MagicComment, "annotate_rb/model_annotator/file_parser/magic_comment"
|
|
8
10
|
autoload :ParsedFile, "annotate_rb/model_annotator/file_parser/parsed_file"
|
|
9
11
|
autoload :ParsedFileResult, "annotate_rb/model_annotator/file_parser/parsed_file_result"
|
|
10
12
|
autoload :YmlParser, "annotate_rb/model_annotator/file_parser/yml_parser"
|
data/lib/annotate_rb/model_annotator/foreign_key_annotation/foreign_key_component_builder.rb
CHANGED
|
@@ -32,6 +32,12 @@ module AnnotateRb
|
|
|
32
32
|
constraints_info = ""
|
|
33
33
|
constraints_info += "ON DELETE => #{foreign_key.on_delete} " if foreign_key.on_delete
|
|
34
34
|
constraints_info += "ON UPDATE => #{foreign_key.on_update} " if foreign_key.on_update
|
|
35
|
+
if foreign_key.respond_to?(:deferrable) && foreign_key.deferrable
|
|
36
|
+
# Rails 7.0's extract_foreign_key_deferrable returns `true` for the initially-immediate case;
|
|
37
|
+
# 7.1+ normalized this to `:immediate`. Map `true` back to IMMEDIATE so we don't emit "INITIALLY TRUE".
|
|
38
|
+
initially = (foreign_key.deferrable == true) ? "IMMEDIATE" : foreign_key.deferrable.to_s.upcase
|
|
39
|
+
constraints_info += "DEFERRABLE INITIALLY #{initially} "
|
|
40
|
+
end
|
|
35
41
|
constraints_info.strip
|
|
36
42
|
end
|
|
37
43
|
end
|
|
@@ -15,6 +15,9 @@ module AnnotateRb
|
|
|
15
15
|
indexes = @model.retrieve_indexes_from_table
|
|
16
16
|
return Components::NilComponent.new if indexes.empty?
|
|
17
17
|
|
|
18
|
+
indexes = reject_constraint_backed_indexes(indexes)
|
|
19
|
+
return Components::NilComponent.new if indexes.empty?
|
|
20
|
+
|
|
18
21
|
max_size = indexes.map { |index| index.name.size }.max + 1
|
|
19
22
|
|
|
20
23
|
indexes = indexes.sort_by(&:name).map do |index|
|
|
@@ -23,6 +26,32 @@ module AnnotateRb
|
|
|
23
26
|
|
|
24
27
|
_annotation = Annotation.new(indexes)
|
|
25
28
|
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Mirrors ActiveRecord's schema_dumper#indexes_in_create: PostgreSQL's
|
|
33
|
+
# unique and exclusion constraints are backed by indexes with the same
|
|
34
|
+
# name, so those show up in `connection.indexes` too. Drop them here so
|
|
35
|
+
# they only appear under their dedicated sections.
|
|
36
|
+
def reject_constraint_backed_indexes(indexes)
|
|
37
|
+
connection = @model.connection
|
|
38
|
+
|
|
39
|
+
if connection.respond_to?(:supports_exclusion_constraints?) &&
|
|
40
|
+
connection.supports_exclusion_constraints? &&
|
|
41
|
+
connection.respond_to?(:exclusion_constraints)
|
|
42
|
+
excl_names = connection.exclusion_constraints(@model.table_name).map(&:name)
|
|
43
|
+
indexes = indexes.reject { |index| excl_names.include?(index.name) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if connection.respond_to?(:supports_unique_constraints?) &&
|
|
47
|
+
connection.supports_unique_constraints? &&
|
|
48
|
+
connection.respond_to?(:unique_constraints)
|
|
49
|
+
unique_names = connection.unique_constraints(@model.table_name).map(&:name)
|
|
50
|
+
indexes = indexes.reject { |index| unique_names.include?(index.name) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
indexes
|
|
54
|
+
end
|
|
26
55
|
end
|
|
27
56
|
end
|
|
28
57
|
end
|
|
@@ -45,16 +45,27 @@ module AnnotateRb
|
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
comment_info = ""
|
|
49
|
+
if options[:show_indexes_comments]
|
|
50
|
+
value = index.try(:comment).try(:to_s)
|
|
51
|
+
comment_info = if value.present?
|
|
52
|
+
" COMMENT #{value}"
|
|
53
|
+
else
|
|
54
|
+
""
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
48
58
|
# standard:disable Lint/FormatParameterMismatch
|
|
49
59
|
sprintf(
|
|
50
|
-
"# %-#{max_size}.#{max_size}s %s%s%s%s%s%s",
|
|
60
|
+
"# %-#{max_size}.#{max_size}s %s%s%s%s%s%s%s",
|
|
51
61
|
index.name,
|
|
52
62
|
"(#{columns_info.join(",")})",
|
|
53
63
|
include_info,
|
|
54
64
|
unique_info,
|
|
55
65
|
nulls_not_distinct_info,
|
|
56
66
|
where_info,
|
|
57
|
-
using_info
|
|
67
|
+
using_info,
|
|
68
|
+
comment_info
|
|
58
69
|
).rstrip
|
|
59
70
|
# standard:enable Lint/FormatParameterMismatch
|
|
60
71
|
end
|
|
@@ -92,13 +103,24 @@ module AnnotateRb
|
|
|
92
103
|
end
|
|
93
104
|
end
|
|
94
105
|
|
|
106
|
+
comment_info = ""
|
|
107
|
+
if options[:show_indexes_comments]
|
|
108
|
+
value = index.try(:comment).try(:to_s)
|
|
109
|
+
comment_info = if value.present?
|
|
110
|
+
" _comment_ #{value}"
|
|
111
|
+
else
|
|
112
|
+
""
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
95
116
|
details = sprintf(
|
|
96
|
-
"%s%s%s%s%s",
|
|
117
|
+
"%s%s%s%s%s%s",
|
|
97
118
|
include_info,
|
|
98
119
|
unique_info,
|
|
99
120
|
nulls_not_distinct_info,
|
|
100
121
|
where_info,
|
|
101
|
-
using_info
|
|
122
|
+
using_info,
|
|
123
|
+
comment_info
|
|
102
124
|
).strip
|
|
103
125
|
details = " (#{details})" unless details.blank?
|
|
104
126
|
|
|
@@ -68,8 +68,20 @@ module AnnotateRb
|
|
|
68
68
|
connection.table_comment(@klass.table_name).present?
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
# Returns column defaults for annotations.
|
|
72
|
+
#
|
|
73
|
+
# `Model#column_defaults` reflects `attribute :foo, default: X` overrides,
|
|
74
|
+
# which would incorrectly show the Ruby-side default in annotations
|
|
75
|
+
# instead of the DB schema default. To preserve model-level decorations
|
|
76
|
+
# such as `TimeZoneConverter` on datetime columns, we start from
|
|
77
|
+
# `column_defaults` and only substitute the DB schema value when a
|
|
78
|
+
# difference indicates an attribute-level override.
|
|
71
79
|
def column_defaults
|
|
72
|
-
@klass.column_defaults
|
|
80
|
+
@column_defaults ||= @klass.column_defaults.each_with_object({}) do |(name, value), result|
|
|
81
|
+
column = @klass.columns_hash[name]
|
|
82
|
+
schema_value = schema_default_for(column)
|
|
83
|
+
result[name] = (value == schema_value) ? value : schema_value
|
|
84
|
+
end
|
|
73
85
|
end
|
|
74
86
|
|
|
75
87
|
# Add columns managed by the globalize gem if this gem is being used.
|
|
@@ -219,6 +231,23 @@ module AnnotateRb
|
|
|
219
231
|
]
|
|
220
232
|
end
|
|
221
233
|
|
|
234
|
+
def enum_types
|
|
235
|
+
@enum_types ||=
|
|
236
|
+
if connection.respond_to?(:enum_types)
|
|
237
|
+
begin
|
|
238
|
+
# enum values may be a String or an Array depending on the Rails version.
|
|
239
|
+
# See: https://github.com/rails/rails/pull/54141
|
|
240
|
+
connection.enum_types.map do |name, values|
|
|
241
|
+
[name, values.is_a?(Array) ? values : values.to_s.split(",")]
|
|
242
|
+
end
|
|
243
|
+
rescue ActiveRecord::StatementInvalid
|
|
244
|
+
[]
|
|
245
|
+
end
|
|
246
|
+
else
|
|
247
|
+
[]
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
222
251
|
def migration_version
|
|
223
252
|
return 0 unless @options[:include_version]
|
|
224
253
|
|
|
@@ -229,7 +258,14 @@ module AnnotateRb
|
|
|
229
258
|
|
|
230
259
|
if @options.get_state(cache_key).nil?
|
|
231
260
|
migration_version = begin
|
|
232
|
-
|
|
261
|
+
# Rails 7.1+ moved migration_context from ConnectionAdapter to ConnectionPool.
|
|
262
|
+
# ConnectionAdapter#migration_context was removed in Rails 7.2.
|
|
263
|
+
# See: https://github.com/rails/rails/pull/51162
|
|
264
|
+
if connection.pool.respond_to?(:migration_context)
|
|
265
|
+
connection.pool.migration_context.current_version
|
|
266
|
+
else
|
|
267
|
+
connection.migration_context.current_version
|
|
268
|
+
end
|
|
233
269
|
rescue
|
|
234
270
|
0
|
|
235
271
|
end
|
|
@@ -239,6 +275,26 @@ module AnnotateRb
|
|
|
239
275
|
|
|
240
276
|
@options.get_state(cache_key)
|
|
241
277
|
end
|
|
278
|
+
|
|
279
|
+
private
|
|
280
|
+
|
|
281
|
+
def schema_default_for(column)
|
|
282
|
+
return nil if column.nil? || column.default.nil? || column.default_function
|
|
283
|
+
cast_type_for(column).deserialize(column.default)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Rails post-8.1 exposes `Column#cast_type` directly; Rails 8.1 introduced
|
|
287
|
+
# the transitional `Column#fetch_cast_type(connection)`; older versions
|
|
288
|
+
# required `connection.lookup_cast_type_from_column(column)`.
|
|
289
|
+
def cast_type_for(column)
|
|
290
|
+
if column.respond_to?(:cast_type)
|
|
291
|
+
column.cast_type
|
|
292
|
+
elsif column.respond_to?(:fetch_cast_type)
|
|
293
|
+
column.fetch_cast_type(connection)
|
|
294
|
+
else
|
|
295
|
+
connection.lookup_cast_type_from_column(column)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
242
298
|
end
|
|
243
299
|
end
|
|
244
300
|
end
|