danger-packwerk 0.14.0 → 0.14.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/lib/danger-packwerk/danger_package_todo_yml_changes.rb +21 -118
  3. data/lib/danger-packwerk/danger_packwerk.rb +12 -7
  4. data/lib/danger-packwerk/private/git.rb +65 -0
  5. data/lib/danger-packwerk/private/todo_yml_changes.rb +135 -0
  6. data/lib/danger-packwerk/private.rb +1 -0
  7. data/lib/danger-packwerk/version.rb +1 -1
  8. metadata +4 -88
  9. data/sorbet/config +0 -4
  10. data/sorbet/rbi/gems/actionview@7.0.4.rbi +0 -11543
  11. data/sorbet/rbi/gems/activesupport@7.0.4.rbi +0 -12959
  12. data/sorbet/rbi/gems/addressable@2.8.1.rbi +0 -1505
  13. data/sorbet/rbi/gems/ast@2.4.2.rbi +0 -522
  14. data/sorbet/rbi/gems/better_html@2.0.1.rbi +0 -286
  15. data/sorbet/rbi/gems/builder@3.2.4.rbi +0 -8
  16. data/sorbet/rbi/gems/claide-plugins@0.9.2.rbi +0 -791
  17. data/sorbet/rbi/gems/claide@1.1.0.rbi +0 -1132
  18. data/sorbet/rbi/gems/code_ownership@1.29.2.rbi +0 -525
  19. data/sorbet/rbi/gems/code_teams@1.0.0.rbi +0 -120
  20. data/sorbet/rbi/gems/coderay@1.1.3.rbi +0 -2256
  21. data/sorbet/rbi/gems/colored2@3.1.2.rbi +0 -130
  22. data/sorbet/rbi/gems/concurrent-ruby@1.1.10.rbi +0 -8695
  23. data/sorbet/rbi/gems/constant_resolver@0.2.0.rbi +0 -30
  24. data/sorbet/rbi/gems/cork@0.3.0.rbi +0 -248
  25. data/sorbet/rbi/gems/crass@1.0.6.rbi +0 -436
  26. data/sorbet/rbi/gems/danger-plugin-api@1.0.0.rbi +0 -8
  27. data/sorbet/rbi/gems/danger@9.0.0.rbi +0 -4722
  28. data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +0 -862
  29. data/sorbet/rbi/gems/erubi@1.11.0.rbi +0 -102
  30. data/sorbet/rbi/gems/faraday-em_http@1.0.0.rbi +0 -266
  31. data/sorbet/rbi/gems/faraday-em_synchrony@1.0.0.rbi +0 -209
  32. data/sorbet/rbi/gems/faraday-excon@1.1.0.rbi +0 -212
  33. data/sorbet/rbi/gems/faraday-http-cache@2.4.1.rbi +0 -805
  34. data/sorbet/rbi/gems/faraday-httpclient@1.0.1.rbi +0 -221
  35. data/sorbet/rbi/gems/faraday-multipart@1.0.4.rbi +0 -266
  36. data/sorbet/rbi/gems/faraday-net_http@1.0.1.rbi +0 -216
  37. data/sorbet/rbi/gems/faraday-net_http_persistent@1.2.0.rbi +0 -206
  38. data/sorbet/rbi/gems/faraday-patron@1.0.0.rbi +0 -212
  39. data/sorbet/rbi/gems/faraday-rack@1.0.0.rbi +0 -225
  40. data/sorbet/rbi/gems/faraday-retry@1.0.3.rbi +0 -222
  41. data/sorbet/rbi/gems/faraday@1.10.2.rbi +0 -1862
  42. data/sorbet/rbi/gems/git@1.12.0.rbi +0 -1936
  43. data/sorbet/rbi/gems/i18n@1.12.0.rbi +0 -1643
  44. data/sorbet/rbi/gems/kramdown-parser-gfm@1.1.0.rbi +0 -8
  45. data/sorbet/rbi/gems/kramdown@2.4.0.rbi +0 -2168
  46. data/sorbet/rbi/gems/loofah@2.19.0.rbi +0 -646
  47. data/sorbet/rbi/gems/method_source@1.0.0.rbi +0 -199
  48. data/sorbet/rbi/gems/minitest@5.16.3.rbi +0 -997
  49. data/sorbet/rbi/gems/multipart-post@2.2.3.rbi +0 -165
  50. data/sorbet/rbi/gems/nap@1.1.0.rbi +0 -351
  51. data/sorbet/rbi/gems/no_proxy_fix@0.1.2.rbi +0 -8
  52. data/sorbet/rbi/gems/nokogiri@1.13.8.rbi +0 -4916
  53. data/sorbet/rbi/gems/octokit@5.6.1.rbi +0 -8939
  54. data/sorbet/rbi/gems/open4@1.3.4.rbi +0 -8
  55. data/sorbet/rbi/gems/packs@0.0.5.rbi +0 -111
  56. data/sorbet/rbi/gems/packwerk@2.2.1-e998ef65194de398f0baaf03a0ba33390b30351e.rbi +0 -2161
  57. data/sorbet/rbi/gems/parallel@1.22.1.rbi +0 -163
  58. data/sorbet/rbi/gems/parse_packwerk@0.18.0.rbi +0 -225
  59. data/sorbet/rbi/gems/parser@3.1.2.1.rbi +0 -5988
  60. data/sorbet/rbi/gems/pry@0.14.1.rbi +0 -6969
  61. data/sorbet/rbi/gems/public_suffix@5.0.0.rbi +0 -779
  62. data/sorbet/rbi/gems/racc@1.6.0.rbi +0 -92
  63. data/sorbet/rbi/gems/rails-dom-testing@2.0.3.rbi +0 -8
  64. data/sorbet/rbi/gems/rails-html-sanitizer@1.4.3.rbi +0 -493
  65. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +0 -227
  66. data/sorbet/rbi/gems/rake@13.0.6.rbi +0 -1865
  67. data/sorbet/rbi/gems/rbi@0.0.14.rbi +0 -2337
  68. data/sorbet/rbi/gems/rchardet@1.8.0.rbi +0 -587
  69. data/sorbet/rbi/gems/regexp_parser@2.5.0.rbi +0 -1851
  70. data/sorbet/rbi/gems/rexml@3.2.5.rbi +0 -3852
  71. data/sorbet/rbi/gems/rspec-core@3.11.0.rbi +0 -7725
  72. data/sorbet/rbi/gems/rspec-expectations@3.11.0.rbi +0 -6201
  73. data/sorbet/rbi/gems/rspec-mocks@3.11.1.rbi +0 -3625
  74. data/sorbet/rbi/gems/rspec-support@3.11.0.rbi +0 -1176
  75. data/sorbet/rbi/gems/rspec@3.11.0.rbi +0 -40
  76. data/sorbet/rbi/gems/rubocop-ast@1.21.0.rbi +0 -4193
  77. data/sorbet/rbi/gems/rubocop-sorbet@0.6.8.rbi +0 -677
  78. data/sorbet/rbi/gems/rubocop@1.36.0.rbi +0 -37914
  79. data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +0 -732
  80. data/sorbet/rbi/gems/ruby2_keywords@0.0.5.rbi +0 -8
  81. data/sorbet/rbi/gems/sawyer@0.9.2.rbi +0 -513
  82. data/sorbet/rbi/gems/smart_properties@1.17.0.rbi +0 -326
  83. data/sorbet/rbi/gems/spoom@1.1.11.rbi +0 -1600
  84. data/sorbet/rbi/gems/tapioca@0.8.0.rbi +0 -1959
  85. data/sorbet/rbi/gems/terminal-table@3.0.2.rbi +0 -438
  86. data/sorbet/rbi/gems/thor@1.2.1.rbi +0 -2921
  87. data/sorbet/rbi/gems/tzinfo@2.0.5.rbi +0 -4879
  88. data/sorbet/rbi/gems/unicode-display_width@2.3.0.rbi +0 -27
  89. data/sorbet/rbi/gems/unparser@0.6.5.rbi +0 -2789
  90. data/sorbet/rbi/gems/webrick@1.7.0.rbi +0 -1802
  91. data/sorbet/rbi/gems/yard-sorbet@0.6.1.rbi +0 -288
  92. data/sorbet/rbi/gems/yard@0.9.27.rbi +0 -12668
  93. data/sorbet/rbi/todo.rbi +0 -125
  94. data/sorbet/tapioca/require.rb +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa564ef118a2a39e97526ac0e2a760d9fb42e721d2f85bd4bde334f0493d1246
4
- data.tar.gz: 34ae63e01e035b03c2110550facba1dbf62ffd025907c3219319d033665120e8
3
+ metadata.gz: 11de71d95ae9ff78daff3f87835d184e206d6088ecece8608e03d7e42eaa0424
4
+ data.tar.gz: 9386a9c42dbc1ef0fface734b0c6e1b9aa031629c46d3961364a37b1afda1b36
5
5
  SHA512:
6
- metadata.gz: 6a724b6f6ffee5964fcd48c1638b10045e609c7ef880299c9a1a5ca5edaee0eee5753fd0d77b650349d1a0936b58a88697adcbb290e967c756aa35229e593d46
7
- data.tar.gz: cbc524de180c63511901b118e0aeac8db39ba5432977c27529765dc235e10a66005df8949851996d92febf3c7121082a214d5f72bbd1e85ac58b30b90272bdfc
6
+ metadata.gz: 0114bf7aa642b184bd8ba990764eff04925bafd7defc7c3c77af8b4edcef5c576c8412471d5ab31e3150df4298582774b5f1943ad3106c04b3b0fa02c619fae6
7
+ data.tar.gz: 05ec9637958c0f80a2f2ec0cd3d25578081fc015d98b9f1a7a5b296ef3a8c8e2304319ed6af8c3b4ceffb5a4e5e5f421596f92593ed1002690f60cbb465bd5f3
@@ -29,22 +29,25 @@ module DangerPackwerk
29
29
  offenses_formatter: T.nilable(Update::OffensesFormatter),
30
30
  before_comment: BeforeComment,
31
31
  max_comments: Integer,
32
- violation_types: T::Array[String]
32
+ violation_types: T::Array[String],
33
+ root_path: T.nilable(String)
33
34
  ).void
34
35
  end
35
36
  def check(
36
37
  offenses_formatter: nil,
37
38
  before_comment: DEFAULT_BEFORE_COMMENT,
38
39
  max_comments: DEFAULT_MAX_COMMENTS,
39
- violation_types: DEFAULT_VIOLATION_TYPES
40
+ violation_types: DEFAULT_VIOLATION_TYPES,
41
+ root_path: nil
40
42
  )
41
43
  offenses_formatter ||= Update::DefaultFormatter.new
42
44
  repo_link = github.pr_json[:base][:repo][:html_url]
43
45
  org_name = github.pr_json[:base][:repo][:owner][:login]
44
46
 
45
- changed_package_todo_ymls = (git.modified_files + git.added_files + git.deleted_files).grep(PACKAGE_TODO_PATTERN)
47
+ git_filesystem = Private::GitFilesystem.new(git: git, root: root_path || '')
48
+ changed_package_todo_ymls = (git_filesystem.modified_files + git_filesystem.added_files + git_filesystem.deleted_files).grep(PACKAGE_TODO_PATTERN)
46
49
 
47
- violation_diff = get_violation_diff(violation_types)
50
+ violation_diff = get_violation_diff(violation_types, root_path: root_path)
48
51
 
49
52
  before_comment.call(
50
53
  violation_diff,
@@ -61,130 +64,30 @@ module DangerPackwerk
61
64
  markdown(
62
65
  offenses_formatter.format_offenses(violations, repo_link, org_name),
63
66
  line: location.line_number,
64
- file: location.file
67
+ file: git_filesystem.convert_to_filesystem(location.file)
65
68
  )
66
69
 
67
70
  current_comment_count += 1
68
71
  end
69
72
  end
70
73
 
71
- sig { params(violation_types: T::Array[String]).returns(ViolationDiff) }
72
- def get_violation_diff(violation_types)
73
- added_violations = T.let([], T::Array[BasicReferenceOffense])
74
- removed_violations = T.let([], T::Array[BasicReferenceOffense])
75
-
76
- git.added_files.grep(PACKAGE_TODO_PATTERN).each do |added_package_todo_yml_file|
77
- # Since the file is added, we know on the base commit there are no violations related to this pack,
78
- # and that all violations from this file are new
79
- added_violations += BasicReferenceOffense.from(added_package_todo_yml_file)
80
- end
81
-
82
- git.deleted_files.grep(PACKAGE_TODO_PATTERN).each do |deleted_package_todo_yml_file|
83
- # Since the file is deleted, we know on the HEAD commit there are no violations related to this pack,
84
- # and that all violations from this file are deleted
85
- deleted_violations = get_violations_before_patch_for(deleted_package_todo_yml_file)
86
- removed_violations += deleted_violations
87
- end
88
-
89
- # The format for git.renamed_files is a T::Array[{after: "some/path/new", before: "some/path/old"}]
90
- renamed_files_before = git.renamed_files.map { |before_after_file| before_after_file[:before] }
91
- renamed_files_after = git.renamed_files.map { |before_after_file| before_after_file[:after] }
92
-
93
- git.modified_files.grep(PACKAGE_TODO_PATTERN).each do |modified_package_todo_yml_file|
94
- # We skip over modified files if one of the modified files is a renamed `package_todo.yml` file.
95
- # This allows us to rename packs while ignoring "new violations" in those renamed packs.
96
- next if renamed_files_before.include?(modified_package_todo_yml_file)
97
-
98
- head_commit_violations = BasicReferenceOffense.from(modified_package_todo_yml_file)
99
- base_commit_violations = get_violations_before_patch_for(modified_package_todo_yml_file)
100
- added_violations += head_commit_violations - base_commit_violations
101
- removed_violations += base_commit_violations - head_commit_violations
102
- end
103
-
104
- #
105
- # This implementation creates some false negatives:
106
- # That is – it doesn't capture some cases:
107
- # 1) A file has been renamed without renaming a constant.
108
- # That can happen if we change only the autoloaded portion of a filename.
109
- # For example: `packs/foo/app/services/my_class.rb` (defines: `MyClass`)
110
- # is changed to `packs/foo/app/public/my_class.rb` (still defines: `MyClass`)
111
- #
112
- # This implementation also doesn't cover these false positives:
113
- # That is – it leaves a comment when it should not.
114
- # 1) A CONSTANT within a class or module has been renamed.
115
- # e.g. `class MyClass; MY_CONSTANT = 1; end` becomes `class MyClass; RENAMED_CONSTANT = 1; end`
116
- # We would not detect based on file renames that `MY_CONSTANT` has been renamed.
117
- #
118
- renamed_constants = []
119
-
120
- added_violations.each do |violation|
121
- filepath_that_defines_this_constant = Private.constant_resolver.resolve(violation.class_name)&.location
122
- renamed_constants << violation.class_name if renamed_files_after.include?(filepath_that_defines_this_constant)
123
- end
124
-
125
- relevant_added_violations = added_violations.reject do |violation|
126
- renamed_files_after.include?(violation.file) ||
127
- renamed_constants.include?(violation.class_name) ||
128
- !violation_types.include?(violation.type)
129
- end
74
+ sig do
75
+ params(
76
+ violation_types: T::Array[String],
77
+ root_path: T.nilable(String)
78
+ ).returns(ViolationDiff)
79
+ end
80
+ def get_violation_diff(violation_types, root_path: nil)
81
+ git_filesystem = Private::GitFilesystem.new(git: git, root: root_path || '')
130
82
 
131
- relevant_removed_violations = removed_violations.select do |violation|
132
- violation_types.include?(violation.type)
133
- end
83
+ added_violations, removed_violations = Private::TodoYmlChanges.get_reference_offenses(
84
+ violation_types, git_filesystem
85
+ )
134
86
 
135
87
  ViolationDiff.new(
136
- added_violations: relevant_added_violations,
137
- removed_violations: relevant_removed_violations
88
+ added_violations: added_violations,
89
+ removed_violations: removed_violations
138
90
  )
139
91
  end
140
-
141
- private
142
-
143
- sig { params(package_todo_yml_file: String).returns(T::Array[BasicReferenceOffense]) }
144
- def get_violations_before_patch_for(package_todo_yml_file)
145
- # The strategy to get the violations before this PR is to reverse the patch on each `package_todo.yml`.
146
- # A previous strategy attempted to use `git merge-base --fork-point`, but there are many situations where it returns
147
- # empty values. That strategy is fickle because it depends on the state of the `reflog` within the CI suite, which appears
148
- # to not be reliable to depend on.
149
- #
150
- # Instead, just inverting the patch should hopefully provide a more reliable way to figure out what was the state of the file before
151
- # the PR without needing to use git commands that interpret the branch history based on local git history.
152
- #
153
- # We apply the patch to the original file so that we can seamlessly reverse the patch applied to that file (since patches are coupled to
154
- # the files they modify). After parsing the violations from that `package_todo.yml` file with the patch reversed,
155
- # we use a temporary copy of the original file to rewrite to it with the original contents.
156
- # Note that practically speaking, we don't need to rewrite the original contents (since we already fetched the
157
- # original contents above and the CI file system should be ephemeral). However, we do this anyways in case we later change these
158
- # assumptions, or another client's environment is different and expects these files not to be mutated.
159
-
160
- # Keep track of the original file contents. If the original file has been deleted, then we delete the file after inverting the patch at the end, rather than rewriting it.
161
- package_todo_yml_file_copy = (File.read(package_todo_yml_file) if File.exist?(package_todo_yml_file))
162
-
163
- Tempfile.create do |patch_file|
164
- # Normally we'd use `git.diff_for_file(package_todo_yml_file).patch` here, but there is a bug where it does not work for deleted files yet.
165
- # I have a fix for that here: https://github.com/danger/danger/pull/1357
166
- # Until that lands, I'm just using the underlying implementation of that method to get the diff for a file.
167
- # Note that I might want to use a safe escape operator, `&.patch` and return gracefully if the patch cannot be found.
168
- # However I'd be interested in why that ever happens, so for now going to proceed as is.
169
- # (Note that better yet we'd have observability into these so I can just log under those circumstances rather than surfacing an error to the user,
170
- # but we don't have that quite yet.)
171
- patch_for_file = git.diff[package_todo_yml_file].patch
172
- # This appears to be a known issue that patches require new lines at the end. It seems like this is an issue with Danger that
173
- # it gives us a patch without a newline.
174
- # https://stackoverflow.com/questions/18142870/git-error-fatal-corrupt-patch-at-line-36
175
- patch_file << "#{patch_for_file}\n"
176
- patch_file.rewind
177
- # https://git-scm.com/docs/git-apply
178
- _stdout, _stderr, _status = Open3.capture3("git apply --reverse #{patch_file.path}")
179
- # https://www.rubyguides.com/2019/05/ruby-tempfile/
180
- BasicReferenceOffense.from(package_todo_yml_file)
181
- end
182
- ensure
183
- if package_todo_yml_file_copy
184
- File.write(package_todo_yml_file, package_todo_yml_file_copy)
185
- else
186
- File.delete(package_todo_yml_file)
187
- end
188
- end
189
92
  end
190
93
  end
@@ -6,6 +6,7 @@ require 'packwerk'
6
6
  require 'parse_packwerk'
7
7
  require 'sorbet-runtime'
8
8
  require 'danger-packwerk/packwerk_wrapper'
9
+ require 'danger-packwerk/private/git'
9
10
 
10
11
  module DangerPackwerk
11
12
  # Note that Danger names the plugin (i.e. anything that inherits from `Danger::Plugin`) by taking the name of the class and gsubbing out "Danger"
@@ -43,7 +44,8 @@ module DangerPackwerk
43
44
  failure_message: String,
44
45
  on_failure: OnFailure,
45
46
  violation_types: T::Array[String],
46
- grouping_strategy: CommentGroupingStrategy
47
+ grouping_strategy: CommentGroupingStrategy,
48
+ root_path: T.nilable(String)
47
49
  ).void
48
50
  end
49
51
  def check(
@@ -53,7 +55,8 @@ module DangerPackwerk
53
55
  failure_message: DEFAULT_FAILURE_MESSAGE,
54
56
  on_failure: DEFAULT_ON_FAILURE,
55
57
  violation_types: DEFAULT_VIOLATION_TYPES,
56
- grouping_strategy: CommentGroupingStrategy::PerConstantPerLocation
58
+ grouping_strategy: CommentGroupingStrategy::PerConstantPerLocation,
59
+ root_path: nil
57
60
  )
58
61
  offenses_formatter ||= Check::DefaultFormatter.new
59
62
  repo_link = github.pr_json[:base][:repo][:html_url]
@@ -67,9 +70,12 @@ module DangerPackwerk
67
70
  # trigger the warning message, which is good, since we only want to trigger on new code.
68
71
  github.dismiss_out_of_range_messages
69
72
 
73
+ git_filesystem = Private::GitFilesystem.new(git: git, root: root_path || '')
74
+
70
75
  # https://github.com/danger/danger/blob/eca19719d3e585fe1cc46bc5377f9aa955ebf609/lib/danger/danger_core/plugins/dangerfile_git_plugin.rb#L80
71
- renamed_files_after = git.renamed_files.map { |f| f[:after] }
72
- targeted_files = (git.modified_files + git.added_files + renamed_files_after).select do |f|
76
+ renamed_files_after = git_filesystem.renamed_files.map { |f| f[:after] }
77
+
78
+ targeted_files = (git_filesystem.modified_files + git_filesystem.added_files + renamed_files_after).select do |f|
73
79
  path = Pathname.new(f)
74
80
 
75
81
  # We probably want to check the `include` key of `packwerk.yml`. By default, this value is "**/*.{rb,rake,erb}",
@@ -92,7 +98,7 @@ module DangerPackwerk
92
98
 
93
99
  packwerk_reference_offenses = PackwerkWrapper.get_offenses_for_files(targeted_files.to_a).compact
94
100
 
95
- renamed_files = git.renamed_files.map { |before_after_file| before_after_file[:after] }
101
+ renamed_files = git_filesystem.renamed_files.map { |before_after_file| before_after_file[:after] }
96
102
 
97
103
  packwerk_reference_offenses_to_care_about = packwerk_reference_offenses.reject do |packwerk_reference_offense|
98
104
  constant_name = packwerk_reference_offense.reference.constant.name
@@ -131,8 +137,7 @@ module DangerPackwerk
131
137
  referencing_file = reference_offense.reference.relative_path
132
138
 
133
139
  message = offenses_formatter.format_offenses(unique_packwerk_reference_offenses, repo_link, org_name)
134
-
135
- markdown(message, file: referencing_file, line: line_number)
140
+ markdown(message, file: git_filesystem.convert_to_filesystem(referencing_file), line: line_number)
136
141
  end
137
142
 
138
143
  if current_comment_count > 0
@@ -0,0 +1,65 @@
1
+ # typed: strict
2
+
3
+ require 'code_ownership'
4
+ require 'packs'
5
+
6
+ # In order to support running danger-packwerk from a non-root filepath, we need
7
+ # to wrap some git functions in filesystem wrappers: packwerk runs relative to
8
+ # the rails app root, whereas git returns paths on the actual filesystem.
9
+ module DangerPackwerk
10
+ module Private
11
+ class GitFilesystem < T::Struct
12
+ extend T::Sig
13
+
14
+ const :git, Danger::DangerfileGitPlugin
15
+ const :root, String
16
+
17
+ sig { returns(T::Array[{ after: String, before: String }]) }
18
+ def renamed_files
19
+ @git.renamed_files.map do |f|
20
+ {
21
+ after: convert_file_from_filesystem(f[:after]),
22
+ before: convert_file_from_filesystem(f[:before])
23
+ }
24
+ end
25
+ end
26
+
27
+ sig { returns(T::Array[String]) }
28
+ def modified_files
29
+ convert_from_filesystem(@git.modified_files.to_a)
30
+ end
31
+
32
+ sig { returns(T::Array[String]) }
33
+ def deleted_files
34
+ convert_from_filesystem(@git.deleted_files.to_a)
35
+ end
36
+
37
+ sig { returns(T::Array[String]) }
38
+ def added_files
39
+ convert_from_filesystem(@git.added_files.to_a)
40
+ end
41
+
42
+ sig { params(filename_on_disk: String).returns(::Git::Diff::DiffFile) }
43
+ def diff(filename_on_disk)
44
+ @git.diff[filename_on_disk]
45
+ end
46
+
47
+ sig { params(path: String).returns(String) }
48
+ def convert_to_filesystem(path)
49
+ Pathname(@root).join(path).to_s
50
+ end
51
+
52
+ private
53
+
54
+ sig { params(files: T::Array[String]).returns(T::Array[String]) }
55
+ def convert_from_filesystem(files)
56
+ files.map { |f| convert_file_from_filesystem(f) }
57
+ end
58
+
59
+ sig { params(file: String).returns(String) }
60
+ def convert_file_from_filesystem(file)
61
+ Pathname(file).relative_path_from(@root).to_s
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,135 @@
1
+ # typed: strict
2
+
3
+ module DangerPackwerk
4
+ module Private
5
+ class TodoYmlChanges
6
+ extend T::Sig
7
+ extend T::Helpers
8
+
9
+ sig do
10
+ params(
11
+ violation_types: T::Array[String],
12
+ git_filesystem: GitFilesystem
13
+ ).returns([T::Array[BasicReferenceOffense], T::Array[BasicReferenceOffense]])
14
+ end
15
+ def self.get_reference_offenses(violation_types, git_filesystem)
16
+ added_violations = T.let([], T::Array[BasicReferenceOffense])
17
+ removed_violations = T.let([], T::Array[BasicReferenceOffense])
18
+
19
+ git_filesystem.added_files.grep(PACKAGE_TODO_PATTERN).each do |added_package_todo_yml_file|
20
+ # Since the file is added, we know on the base commit there are no violations related to this pack,
21
+ # and that all violations from this file are new
22
+ added_violations += BasicReferenceOffense.from(added_package_todo_yml_file)
23
+ end
24
+
25
+ git_filesystem.deleted_files.grep(PACKAGE_TODO_PATTERN).each do |deleted_package_todo_yml_file|
26
+ # Since the file is deleted, we know on the HEAD commit there are no violations related to this pack,
27
+ # and that all violations from this file are deleted
28
+ deleted_violations = get_violations_before_patch_for(git_filesystem, deleted_package_todo_yml_file)
29
+ removed_violations += deleted_violations
30
+ end
31
+
32
+ # The format for git.renamed_files is a T::Array[{after: "some/path/new", before: "some/path/old"}]
33
+ renamed_files_before = git_filesystem.renamed_files.map { |before_after_file| before_after_file[:before] }
34
+ renamed_files_after = git_filesystem.renamed_files.map { |before_after_file| before_after_file[:after] }
35
+
36
+ git_filesystem.modified_files.grep(PACKAGE_TODO_PATTERN).each do |modified_package_todo_yml_file|
37
+ # We skip over modified files if one of the modified files is a renamed `package_todo.yml` file.
38
+ # This allows us to rename packs while ignoring "new violations" in those renamed packs.
39
+ next if renamed_files_before.include?(modified_package_todo_yml_file)
40
+
41
+ head_commit_violations = BasicReferenceOffense.from(modified_package_todo_yml_file)
42
+ base_commit_violations = get_violations_before_patch_for(git_filesystem, modified_package_todo_yml_file)
43
+ added_violations += head_commit_violations - base_commit_violations
44
+ removed_violations += base_commit_violations - head_commit_violations
45
+ end
46
+
47
+ #
48
+ # This implementation creates some false negatives:
49
+ # That is – it doesn't capture some cases:
50
+ # 1) A file has been renamed without renaming a constant.
51
+ # That can happen if we change only the autoloaded portion of a filename.
52
+ # For example: `packs/foo/app/services/my_class.rb` (defines: `MyClass`)
53
+ # is changed to `packs/foo/app/public/my_class.rb` (still defines: `MyClass`)
54
+ #
55
+ # This implementation also doesn't cover these false positives:
56
+ # That is – it leaves a comment when it should not.
57
+ # 1) A CONSTANT within a class or module has been renamed.
58
+ # e.g. `class MyClass; MY_CONSTANT = 1; end` becomes `class MyClass; RENAMED_CONSTANT = 1; end`
59
+ # We would not detect based on file renames that `MY_CONSTANT` has been renamed.
60
+ #
61
+ renamed_constants = []
62
+
63
+ added_violations.each do |violation|
64
+ filepath_that_defines_this_constant = Private.constant_resolver.resolve(violation.class_name)&.location
65
+ renamed_constants << violation.class_name if renamed_files_after.include?(filepath_that_defines_this_constant)
66
+ end
67
+
68
+ relevant_added_violations = added_violations.reject do |violation|
69
+ renamed_files_after.include?(violation.file) ||
70
+ renamed_constants.include?(violation.class_name) ||
71
+ !violation_types.include?(violation.type)
72
+ end
73
+
74
+ relevant_removed_violations = removed_violations.select do |violation|
75
+ violation_types.include?(violation.type)
76
+ end
77
+
78
+ [relevant_added_violations, relevant_removed_violations]
79
+ end
80
+
81
+ sig do
82
+ params(
83
+ git_filesystem: GitFilesystem,
84
+ package_todo_yml_file: String
85
+ ).returns(T::Array[BasicReferenceOffense])
86
+ end
87
+ def self.get_violations_before_patch_for(git_filesystem, package_todo_yml_file)
88
+ # The strategy to get the violations before this PR is to reverse the patch on each `package_todo.yml`.
89
+ # A previous strategy attempted to use `git merge-base --fork-point`, but there are many situations where it returns
90
+ # empty values. That strategy is fickle because it depends on the state of the `reflog` within the CI suite, which appears
91
+ # to not be reliable to depend on.
92
+ #
93
+ # Instead, just inverting the patch should hopefully provide a more reliable way to figure out what was the state of the file before
94
+ # the PR without needing to use git commands that interpret the branch history based on local git history.
95
+ #
96
+ # We apply the patch to the original file so that we can seamlessly reverse the patch applied to that file (since patches are coupled to
97
+ # the files they modify). After parsing the violations from that `package_todo.yml` file with the patch reversed,
98
+ # we use a temporary copy of the original file to rewrite to it with the original contents.
99
+ # Note that practically speaking, we don't need to rewrite the original contents (since we already fetched the
100
+ # original contents above and the CI file system should be ephemeral). However, we do this anyways in case we later change these
101
+ # assumptions, or another client's environment is different and expects these files not to be mutated.
102
+
103
+ # Keep track of the original file contents. If the original file has been deleted, then we delete the file after inverting the patch at the end, rather than rewriting it.
104
+ package_todo_yml_file_copy = (File.read(package_todo_yml_file) if File.exist?(package_todo_yml_file))
105
+
106
+ Tempfile.create do |patch_file|
107
+ # Normally we'd use `git.diff_for_file(package_todo_yml_file).patch` here, but there is a bug where it does not work for deleted files yet.
108
+ # I have a fix for that here: https://github.com/danger/danger/pull/1357
109
+ # Until that lands, I'm just using the underlying implementation of that method to get the diff for a file.
110
+ # Note that I might want to use a safe escape operator, `&.patch` and return gracefully if the patch cannot be found.
111
+ # However I'd be interested in why that ever happens, so for now going to proceed as is.
112
+ # (Note that better yet we'd have observability into these so I can just log under those circumstances rather than surfacing an error to the user,
113
+ # but we don't have that quite yet.)
114
+ package_todo_filesystem_path = git_filesystem.convert_to_filesystem(package_todo_yml_file)
115
+ patch_for_file = git_filesystem.diff(package_todo_filesystem_path).patch
116
+ # This appears to be a known issue that patches require new lines at the end. It seems like this is an issue with Danger that
117
+ # it gives us a patch without a newline.
118
+ # https://stackoverflow.com/questions/18142870/git-error-fatal-corrupt-patch-at-line-36
119
+ patch_file << "#{patch_for_file}\n"
120
+ patch_file.rewind
121
+ # https://git-scm.com/docs/git-apply
122
+ _stdout, _stderr, _status = Open3.capture3("git apply --reverse #{patch_file.path}")
123
+ # https://www.rubyguides.com/2019/05/ruby-tempfile/
124
+ BasicReferenceOffense.from(package_todo_yml_file)
125
+ end
126
+ ensure
127
+ if package_todo_yml_file_copy
128
+ File.write(package_todo_yml_file, package_todo_yml_file_copy)
129
+ else
130
+ File.delete(package_todo_yml_file)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -1,6 +1,7 @@
1
1
  # typed: strict
2
2
 
3
3
  require 'danger-packwerk/private/ownership_information'
4
+ require 'danger-packwerk/private/todo_yml_changes'
4
5
  require 'constant_resolver'
5
6
 
6
7
  module DangerPackwerk
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module DangerPackwerk
5
- VERSION = '0.14.0'
5
+ VERSION = '0.14.2'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: danger-packwerk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.14.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gusto Engineers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-30 00:00:00.000000000 Z
11
+ date: 2023-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: code_ownership
@@ -208,98 +208,14 @@ files:
208
208
  - lib/danger-packwerk/danger_packwerk.rb
209
209
  - lib/danger-packwerk/packwerk_wrapper.rb
210
210
  - lib/danger-packwerk/private.rb
211
+ - lib/danger-packwerk/private/git.rb
211
212
  - lib/danger-packwerk/private/ownership_information.rb
213
+ - lib/danger-packwerk/private/todo_yml_changes.rb
212
214
  - lib/danger-packwerk/update/default_formatter.rb
213
215
  - lib/danger-packwerk/update/offenses_formatter.rb
214
216
  - lib/danger-packwerk/version.rb
215
217
  - lib/danger-packwerk/violation_diff.rb
216
218
  - lib/danger_plugin.rb
217
- - sorbet/config
218
- - sorbet/rbi/gems/actionview@7.0.4.rbi
219
- - sorbet/rbi/gems/activesupport@7.0.4.rbi
220
- - sorbet/rbi/gems/addressable@2.8.1.rbi
221
- - sorbet/rbi/gems/ast@2.4.2.rbi
222
- - sorbet/rbi/gems/better_html@2.0.1.rbi
223
- - sorbet/rbi/gems/builder@3.2.4.rbi
224
- - sorbet/rbi/gems/claide-plugins@0.9.2.rbi
225
- - sorbet/rbi/gems/claide@1.1.0.rbi
226
- - sorbet/rbi/gems/code_ownership@1.29.2.rbi
227
- - sorbet/rbi/gems/code_teams@1.0.0.rbi
228
- - sorbet/rbi/gems/coderay@1.1.3.rbi
229
- - sorbet/rbi/gems/colored2@3.1.2.rbi
230
- - sorbet/rbi/gems/concurrent-ruby@1.1.10.rbi
231
- - sorbet/rbi/gems/constant_resolver@0.2.0.rbi
232
- - sorbet/rbi/gems/cork@0.3.0.rbi
233
- - sorbet/rbi/gems/crass@1.0.6.rbi
234
- - sorbet/rbi/gems/danger-plugin-api@1.0.0.rbi
235
- - sorbet/rbi/gems/danger@9.0.0.rbi
236
- - sorbet/rbi/gems/diff-lcs@1.5.0.rbi
237
- - sorbet/rbi/gems/erubi@1.11.0.rbi
238
- - sorbet/rbi/gems/faraday-em_http@1.0.0.rbi
239
- - sorbet/rbi/gems/faraday-em_synchrony@1.0.0.rbi
240
- - sorbet/rbi/gems/faraday-excon@1.1.0.rbi
241
- - sorbet/rbi/gems/faraday-http-cache@2.4.1.rbi
242
- - sorbet/rbi/gems/faraday-httpclient@1.0.1.rbi
243
- - sorbet/rbi/gems/faraday-multipart@1.0.4.rbi
244
- - sorbet/rbi/gems/faraday-net_http@1.0.1.rbi
245
- - sorbet/rbi/gems/faraday-net_http_persistent@1.2.0.rbi
246
- - sorbet/rbi/gems/faraday-patron@1.0.0.rbi
247
- - sorbet/rbi/gems/faraday-rack@1.0.0.rbi
248
- - sorbet/rbi/gems/faraday-retry@1.0.3.rbi
249
- - sorbet/rbi/gems/faraday@1.10.2.rbi
250
- - sorbet/rbi/gems/git@1.12.0.rbi
251
- - sorbet/rbi/gems/i18n@1.12.0.rbi
252
- - sorbet/rbi/gems/kramdown-parser-gfm@1.1.0.rbi
253
- - sorbet/rbi/gems/kramdown@2.4.0.rbi
254
- - sorbet/rbi/gems/loofah@2.19.0.rbi
255
- - sorbet/rbi/gems/method_source@1.0.0.rbi
256
- - sorbet/rbi/gems/minitest@5.16.3.rbi
257
- - sorbet/rbi/gems/multipart-post@2.2.3.rbi
258
- - sorbet/rbi/gems/nap@1.1.0.rbi
259
- - sorbet/rbi/gems/no_proxy_fix@0.1.2.rbi
260
- - sorbet/rbi/gems/nokogiri@1.13.8.rbi
261
- - sorbet/rbi/gems/octokit@5.6.1.rbi
262
- - sorbet/rbi/gems/open4@1.3.4.rbi
263
- - sorbet/rbi/gems/packs@0.0.5.rbi
264
- - sorbet/rbi/gems/packwerk@2.2.1-e998ef65194de398f0baaf03a0ba33390b30351e.rbi
265
- - sorbet/rbi/gems/parallel@1.22.1.rbi
266
- - sorbet/rbi/gems/parse_packwerk@0.18.0.rbi
267
- - sorbet/rbi/gems/parser@3.1.2.1.rbi
268
- - sorbet/rbi/gems/pry@0.14.1.rbi
269
- - sorbet/rbi/gems/public_suffix@5.0.0.rbi
270
- - sorbet/rbi/gems/racc@1.6.0.rbi
271
- - sorbet/rbi/gems/rails-dom-testing@2.0.3.rbi
272
- - sorbet/rbi/gems/rails-html-sanitizer@1.4.3.rbi
273
- - sorbet/rbi/gems/rainbow@3.1.1.rbi
274
- - sorbet/rbi/gems/rake@13.0.6.rbi
275
- - sorbet/rbi/gems/rbi@0.0.14.rbi
276
- - sorbet/rbi/gems/rchardet@1.8.0.rbi
277
- - sorbet/rbi/gems/regexp_parser@2.5.0.rbi
278
- - sorbet/rbi/gems/rexml@3.2.5.rbi
279
- - sorbet/rbi/gems/rspec-core@3.11.0.rbi
280
- - sorbet/rbi/gems/rspec-expectations@3.11.0.rbi
281
- - sorbet/rbi/gems/rspec-mocks@3.11.1.rbi
282
- - sorbet/rbi/gems/rspec-support@3.11.0.rbi
283
- - sorbet/rbi/gems/rspec@3.11.0.rbi
284
- - sorbet/rbi/gems/rubocop-ast@1.21.0.rbi
285
- - sorbet/rbi/gems/rubocop-sorbet@0.6.8.rbi
286
- - sorbet/rbi/gems/rubocop@1.36.0.rbi
287
- - sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi
288
- - sorbet/rbi/gems/ruby2_keywords@0.0.5.rbi
289
- - sorbet/rbi/gems/sawyer@0.9.2.rbi
290
- - sorbet/rbi/gems/smart_properties@1.17.0.rbi
291
- - sorbet/rbi/gems/spoom@1.1.11.rbi
292
- - sorbet/rbi/gems/tapioca@0.8.0.rbi
293
- - sorbet/rbi/gems/terminal-table@3.0.2.rbi
294
- - sorbet/rbi/gems/thor@1.2.1.rbi
295
- - sorbet/rbi/gems/tzinfo@2.0.5.rbi
296
- - sorbet/rbi/gems/unicode-display_width@2.3.0.rbi
297
- - sorbet/rbi/gems/unparser@0.6.5.rbi
298
- - sorbet/rbi/gems/webrick@1.7.0.rbi
299
- - sorbet/rbi/gems/yard-sorbet@0.6.1.rbi
300
- - sorbet/rbi/gems/yard@0.9.27.rbi
301
- - sorbet/rbi/todo.rbi
302
- - sorbet/tapioca/require.rb
303
219
  homepage: https://github.com/rubyatscale/danger-packwerk
304
220
  licenses:
305
221
  - MIT
data/sorbet/config DELETED
@@ -1,4 +0,0 @@
1
- --dir
2
- .
3
- --enable-experimental-requires-ancestor
4
- --ignore=/vendor/bundle