codeclimate-fede 0.85.23

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.
Files changed (101) hide show
  1. checksums.yaml +7 -0
  2. data/bin/check +18 -0
  3. data/bin/codeclimate +21 -0
  4. data/bin/prep-release +45 -0
  5. data/bin/release +41 -0
  6. data/bin/validate-release +18 -0
  7. data/config/engines.yml +322 -0
  8. data/lib/cc/analyzer.rb +50 -0
  9. data/lib/cc/analyzer/bridge.rb +106 -0
  10. data/lib/cc/analyzer/composite_container_listener.rb +21 -0
  11. data/lib/cc/analyzer/container.rb +208 -0
  12. data/lib/cc/analyzer/container/result.rb +74 -0
  13. data/lib/cc/analyzer/container_listener.rb +9 -0
  14. data/lib/cc/analyzer/engine.rb +125 -0
  15. data/lib/cc/analyzer/engine_output.rb +74 -0
  16. data/lib/cc/analyzer/engine_output_filter.rb +36 -0
  17. data/lib/cc/analyzer/engine_output_overrider.rb +31 -0
  18. data/lib/cc/analyzer/filesystem.rb +50 -0
  19. data/lib/cc/analyzer/formatters.rb +21 -0
  20. data/lib/cc/analyzer/formatters/formatter.rb +53 -0
  21. data/lib/cc/analyzer/formatters/html_formatter.rb +415 -0
  22. data/lib/cc/analyzer/formatters/json_formatter.rb +38 -0
  23. data/lib/cc/analyzer/formatters/plain_text_formatter.rb +101 -0
  24. data/lib/cc/analyzer/formatters/spinner.rb +35 -0
  25. data/lib/cc/analyzer/issue.rb +69 -0
  26. data/lib/cc/analyzer/issue_sorter.rb +30 -0
  27. data/lib/cc/analyzer/issue_validations.rb +26 -0
  28. data/lib/cc/analyzer/issue_validations/category_validation.rb +32 -0
  29. data/lib/cc/analyzer/issue_validations/check_name_presence_validation.rb +15 -0
  30. data/lib/cc/analyzer/issue_validations/content_validation.rb +21 -0
  31. data/lib/cc/analyzer/issue_validations/description_presence_validation.rb +15 -0
  32. data/lib/cc/analyzer/issue_validations/location_format_validation.rb +72 -0
  33. data/lib/cc/analyzer/issue_validations/other_locations_format_validation.rb +41 -0
  34. data/lib/cc/analyzer/issue_validations/path_existence_validation.rb +15 -0
  35. data/lib/cc/analyzer/issue_validations/path_is_file_validation.rb +15 -0
  36. data/lib/cc/analyzer/issue_validations/path_presence_validation.rb +15 -0
  37. data/lib/cc/analyzer/issue_validations/relative_path_validation.rb +32 -0
  38. data/lib/cc/analyzer/issue_validations/remediation_points_validation.rb +25 -0
  39. data/lib/cc/analyzer/issue_validations/severity_validation.rb +39 -0
  40. data/lib/cc/analyzer/issue_validations/type_validation.rb +15 -0
  41. data/lib/cc/analyzer/issue_validations/validation.rb +35 -0
  42. data/lib/cc/analyzer/issue_validator.rb +11 -0
  43. data/lib/cc/analyzer/location_description.rb +45 -0
  44. data/lib/cc/analyzer/logging_container_listener.rb +24 -0
  45. data/lib/cc/analyzer/measurement.rb +22 -0
  46. data/lib/cc/analyzer/measurement_validations.rb +16 -0
  47. data/lib/cc/analyzer/measurement_validations/name_validation.rb +23 -0
  48. data/lib/cc/analyzer/measurement_validations/type_validation.rb +15 -0
  49. data/lib/cc/analyzer/measurement_validations/validation.rb +27 -0
  50. data/lib/cc/analyzer/measurement_validations/value_validation.rb +21 -0
  51. data/lib/cc/analyzer/measurement_validator.rb +11 -0
  52. data/lib/cc/analyzer/mounted_path.rb +80 -0
  53. data/lib/cc/analyzer/raising_container_listener.rb +32 -0
  54. data/lib/cc/analyzer/source_buffer.rb +47 -0
  55. data/lib/cc/analyzer/source_extractor.rb +79 -0
  56. data/lib/cc/analyzer/source_fingerprint.rb +40 -0
  57. data/lib/cc/analyzer/statsd_container_listener.rb +51 -0
  58. data/lib/cc/analyzer/validator.rb +38 -0
  59. data/lib/cc/cli.rb +39 -0
  60. data/lib/cc/cli/analyze.rb +90 -0
  61. data/lib/cc/cli/analyze/engine_failure.rb +11 -0
  62. data/lib/cc/cli/command.rb +85 -0
  63. data/lib/cc/cli/console.rb +12 -0
  64. data/lib/cc/cli/engines.rb +5 -0
  65. data/lib/cc/cli/engines/engine_command.rb +15 -0
  66. data/lib/cc/cli/engines/install.rb +35 -0
  67. data/lib/cc/cli/engines/list.rb +18 -0
  68. data/lib/cc/cli/file_store.rb +42 -0
  69. data/lib/cc/cli/global_cache.rb +47 -0
  70. data/lib/cc/cli/global_config.rb +35 -0
  71. data/lib/cc/cli/help.rb +51 -0
  72. data/lib/cc/cli/output.rb +34 -0
  73. data/lib/cc/cli/prepare.rb +98 -0
  74. data/lib/cc/cli/runner.rb +75 -0
  75. data/lib/cc/cli/validate_config.rb +84 -0
  76. data/lib/cc/cli/version.rb +16 -0
  77. data/lib/cc/cli/version_checker.rb +107 -0
  78. data/lib/cc/config.rb +70 -0
  79. data/lib/cc/config/checks_adapter.rb +40 -0
  80. data/lib/cc/config/default_adapter.rb +54 -0
  81. data/lib/cc/config/engine.rb +41 -0
  82. data/lib/cc/config/engine_set.rb +47 -0
  83. data/lib/cc/config/json_adapter.rb +17 -0
  84. data/lib/cc/config/prepare.rb +92 -0
  85. data/lib/cc/config/validation/check_validator.rb +34 -0
  86. data/lib/cc/config/validation/engine_validator.rb +93 -0
  87. data/lib/cc/config/validation/fetch_validator.rb +78 -0
  88. data/lib/cc/config/validation/file_validator.rb +112 -0
  89. data/lib/cc/config/validation/hash_validations.rb +52 -0
  90. data/lib/cc/config/validation/json.rb +31 -0
  91. data/lib/cc/config/validation/prepare_validator.rb +40 -0
  92. data/lib/cc/config/validation/yaml.rb +66 -0
  93. data/lib/cc/config/yaml_adapter.rb +73 -0
  94. data/lib/cc/engine_registry.rb +74 -0
  95. data/lib/cc/resolv.rb +39 -0
  96. data/lib/cc/workspace.rb +39 -0
  97. data/lib/cc/workspace/exclusion.rb +34 -0
  98. data/lib/cc/workspace/path_tree.rb +49 -0
  99. data/lib/cc/workspace/path_tree/dir_node.rb +67 -0
  100. data/lib/cc/workspace/path_tree/file_node.rb +31 -0
  101. metadata +277 -0
@@ -0,0 +1,9 @@
1
+ module CC
2
+ module Analyzer
3
+ class ContainerListener
4
+ def started(_engine, _details); end
5
+
6
+ def finished(_engine, _details, _result); end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,125 @@
1
+ require "securerandom"
2
+
3
+ module CC
4
+ module Analyzer
5
+ #
6
+ # Running specifically an Engine container
7
+ #
8
+ # Input:
9
+ # - name
10
+ # - metadata
11
+ # - image
12
+ # - command (optional)
13
+ # - config (becomes /config.json)
14
+ # - label
15
+ # - io (to write filtered, validated output)
16
+ #
17
+ # Output:
18
+ # - Container::Result
19
+ #
20
+ class Engine
21
+ Error = Class.new(StandardError)
22
+
23
+ def initialize(name, metadata, config, label)
24
+ @name = name
25
+ @metadata = metadata
26
+ @config = config
27
+ @label = label.to_s
28
+ @error = nil
29
+ end
30
+
31
+ def run(io)
32
+ write_config_file
33
+
34
+ container = Container.new(
35
+ image: metadata.fetch("image"),
36
+ command: metadata["command"],
37
+ name: container_name,
38
+ )
39
+
40
+ container.on_output("\0") do |output|
41
+ handle_output(container, io, output)
42
+ end
43
+
44
+ container.run(container_options).tap do |result|
45
+ result.merge_from_exception(error) if error.present?
46
+ end
47
+ ensure
48
+ delete_config_file
49
+ end
50
+
51
+ private
52
+
53
+ attr_reader :name, :metadata
54
+ attr_accessor :error
55
+
56
+ def handle_output(container, io, raw_output)
57
+ output = EngineOutput.new(name, raw_output)
58
+
59
+ return if output_filter.filter?(output)
60
+
61
+ unless output.valid?
62
+ self.error = Error.new("engine produced invalid output: #{output.error}")
63
+ container.stop("output invalid")
64
+ end
65
+
66
+ unless io.write(output_overrider.apply(output).to_json)
67
+ self.error = Error.new("#{io.class}#write returned false, indicating an error")
68
+ container.stop("output error")
69
+ end
70
+ end
71
+
72
+ def qualified_name
73
+ "#{name}:#{@config.fetch("channel", "stable")}"
74
+ end
75
+
76
+ def container_options
77
+ options = [
78
+ "--cap-drop", "all",
79
+ "--label", "com.codeclimate.label=#{@label}",
80
+ "--log-driver", "none",
81
+ "--memory-swap", "-1",
82
+ "--net", "none",
83
+ "--rm",
84
+ "--volume", "#{code.host_path}:/code:ro",
85
+ "--volume", "#{config_file.host_path}:/config.json:ro",
86
+ "--user", "9000:9000"
87
+ ]
88
+ if (memory = metadata["memory"]).present?
89
+ options.concat(["--memory", memory.to_s])
90
+ end
91
+ options
92
+ end
93
+
94
+ def container_name
95
+ @container_name ||= "cc-engines-#{qualified_name.tr(":", "-")}-#{SecureRandom.uuid}"
96
+ end
97
+
98
+ def write_config_file
99
+ @config["debug"] = ENV["CODECLIMATE_DEBUG"]
100
+ Analyzer.logger.debug "/config.json content: #{@config.inspect}"
101
+ config_file.write(@config.to_json)
102
+ end
103
+
104
+ def delete_config_file
105
+ config_file.delete if config_file.file?
106
+ end
107
+
108
+ def code
109
+ @code ||= MountedPath.code
110
+ end
111
+
112
+ def config_file
113
+ @config_file ||= MountedPath.tmp.join(SecureRandom.uuid)
114
+ end
115
+
116
+ def output_filter
117
+ @output_filter ||= EngineOutputFilter.new(@config)
118
+ end
119
+
120
+ def output_overrider
121
+ @output_overrider ||= EngineOutputOverrider.new(@config)
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,74 @@
1
+ module CC
2
+ module Analyzer
3
+ class EngineOutput
4
+ delegate :blank?, to: :raw_output
5
+
6
+ def initialize(name, raw_output)
7
+ @name = name
8
+ @raw_output = raw_output
9
+ end
10
+
11
+ def issue?
12
+ valid_with_type?("issue")
13
+ end
14
+
15
+ def measurement?
16
+ valid_with_type?("measurement")
17
+ end
18
+
19
+ def as_issue
20
+ Issue.new(name, raw_output)
21
+ end
22
+
23
+ def to_json
24
+ if issue?
25
+ as_issue.to_json
26
+ elsif measurement?
27
+ Measurement.new(name, raw_output).to_json
28
+ end
29
+ end
30
+
31
+ def valid?
32
+ valid_json? && validator && validator.valid?
33
+ end
34
+
35
+ def error
36
+ if !valid_json?
37
+ { message: "Invalid JSON", output: raw_output }
38
+ elsif !validator.present?
39
+ { message: "Unsupported document type", output: raw_output }
40
+ else
41
+ validator.error
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ attr_accessor :name, :raw_output
48
+
49
+ def valid_json?
50
+ parsed_output.present?
51
+ end
52
+
53
+ def valid_with_type?(type)
54
+ parsed_output &&
55
+ parsed_output["type"].present? &&
56
+ parsed_output["type"].downcase == type
57
+ end
58
+
59
+ def parsed_output
60
+ @parsed_output ||= JSON.parse(raw_output)
61
+ rescue JSON::ParserError
62
+ nil
63
+ end
64
+
65
+ def validator
66
+ if issue?
67
+ IssueValidator.new(parsed_output)
68
+ elsif measurement?
69
+ MeasurementValidator.new(parsed_output)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,36 @@
1
+ module CC
2
+ module Analyzer
3
+ class EngineOutputFilter
4
+ ISSUE_TYPE = "issue".freeze
5
+
6
+ def initialize(config = {})
7
+ @config = config
8
+ end
9
+
10
+ def filter?(output)
11
+ output.blank? || (output.issue? && ignore_issue?(output.as_issue))
12
+ end
13
+
14
+ private
15
+
16
+ def ignore_issue?(issue)
17
+ check_disabled?(issue) || ignore_fingerprint?(issue)
18
+ end
19
+
20
+ def check_disabled?(issue)
21
+ !check_config(issue.check_name).fetch("enabled", true)
22
+ end
23
+
24
+ def ignore_fingerprint?(issue)
25
+ @config.fetch("exclude_fingerprints", []).include?(issue.fingerprint)
26
+ rescue SourceExtractor::InvalidLocation
27
+ false
28
+ end
29
+
30
+ def check_config(check_name)
31
+ @checks ||= @config.fetch("checks", {})
32
+ @checks.fetch(check_name, {})
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,31 @@
1
+ module CC
2
+ module Analyzer
3
+ class EngineOutputOverrider
4
+ def initialize(config = {})
5
+ @config = config
6
+ end
7
+
8
+ def apply(output)
9
+ if output.issue?
10
+ override_severity(output.as_issue.as_json)
11
+ else
12
+ output
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :config
19
+
20
+ def override_severity(issue)
21
+ issue.merge(override("severity"))
22
+ end
23
+
24
+ def override(name)
25
+ config.
26
+ fetch("issue_override", {}).
27
+ slice(name)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,50 @@
1
+ module CC
2
+ module Analyzer
3
+ class Filesystem
4
+ attr_reader :root
5
+
6
+ def initialize(root)
7
+ @root = root
8
+ end
9
+
10
+ def exist?(path)
11
+ File.exist?(path_for(path))
12
+ end
13
+
14
+ def source_buffer_for(path)
15
+ SourceBuffer.new(path, read_path(path))
16
+ end
17
+
18
+ def read_path(path)
19
+ File.read(path_for(path))
20
+ end
21
+
22
+ def write_path(path, content)
23
+ File.write(path_for(path), content)
24
+ File.chown(root_uid, root_gid, path_for(path))
25
+ end
26
+
27
+ def ls
28
+ Dir.entries(root).reject { |entry| [".", ".."].include?(entry) }
29
+ end
30
+
31
+ private
32
+
33
+ def path_for(path)
34
+ File.join(root, path)
35
+ end
36
+
37
+ def root_uid
38
+ root_stat.uid
39
+ end
40
+
41
+ def root_gid
42
+ root_stat.gid
43
+ end
44
+
45
+ def root_stat
46
+ @root_stat ||= File.stat(root)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,21 @@
1
+ module CC
2
+ module Analyzer
3
+ module Formatters
4
+ autoload :Formatter, "cc/analyzer/formatters/formatter"
5
+ autoload :HTMLFormatter, "cc/analyzer/formatters/html_formatter"
6
+ autoload :JSONFormatter, "cc/analyzer/formatters/json_formatter"
7
+ autoload :PlainTextFormatter, "cc/analyzer/formatters/plain_text_formatter"
8
+ autoload :Spinner, "cc/analyzer/formatters/spinner"
9
+
10
+ FORMATTERS = {
11
+ html: HTMLFormatter,
12
+ json: JSONFormatter,
13
+ text: PlainTextFormatter,
14
+ }.freeze
15
+
16
+ def self.resolve(name)
17
+ FORMATTERS[name.to_sym] or raise Formatter::InvalidFormatterError, "'#{name}' is not a valid formatter. Valid options are: #{FORMATTERS.keys.join(", ")}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,53 @@
1
+ module CC
2
+ module Analyzer
3
+ module Formatters
4
+ class Formatter
5
+ def initialize(filesystem, output = $stdout)
6
+ @filesystem = filesystem
7
+ @output = output
8
+ end
9
+
10
+ def write(data)
11
+ json = JSON.parse(data)
12
+ json["engine_name"] = current_engine.name
13
+
14
+ case json["type"].downcase
15
+ when "issue"
16
+ issues << json
17
+ when "warning"
18
+ warnings << json
19
+ when "measurement"
20
+ measurements << json
21
+ else
22
+ raise "Invalid type found: #{json["type"]}"
23
+ end
24
+ end
25
+
26
+ def started
27
+ end
28
+
29
+ def engine_running(engine)
30
+ @current_engine = engine
31
+ yield
32
+ ensure
33
+ @current_engine = nil
34
+ end
35
+
36
+ def finished
37
+ end
38
+
39
+ def close
40
+ end
41
+
42
+ def failed(_output)
43
+ end
44
+
45
+ InvalidFormatterError = Class.new(StandardError)
46
+
47
+ private
48
+
49
+ attr_reader :current_engine
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,415 @@
1
+ require "redcarpet"
2
+
3
+ module CC
4
+ module Analyzer
5
+ module Formatters
6
+ class HTMLFormatter < Formatter # rubocop: disable Metrics/ClassLength
7
+ LANGUAGES = Hash.new { |_, ext| ext }.
8
+ merge(
9
+ # abap
10
+ # ada
11
+ "appraisals" => "ruby",
12
+ "as" => "actionscript",
13
+ "asm" => "nasm",
14
+ "bas" => "basic",
15
+ # c
16
+ "c++" => "cpp",
17
+ "capfile" => "ruby",
18
+ "cc" => "cpp",
19
+ "cfc" => "markup",
20
+ "cfm" => "markup",
21
+ "coffee" => "coffeescript",
22
+ "cp" => "cpp",
23
+ # cpp
24
+ "cr" => "crystal",
25
+ "cs" => "csharp",
26
+ "css" => %w[css css-extras],
27
+ "cu" => "cpp",
28
+ "cxx" => "cpp",
29
+ # d
30
+ # dart
31
+ # diff
32
+ "dockerfile" => "docker",
33
+ "dpr" => "pascal",
34
+ "erl" => "erlang",
35
+ "ex" => "elixir",
36
+ "f" => "fortran",
37
+ "f90" => "fortran",
38
+ "f95" => "fortran",
39
+ "feature" => "gherkin",
40
+ "for" => "fortran",
41
+ "fs" => "fsharp",
42
+ "fsi" => "fsharp",
43
+ "fsscript" => "fsharp",
44
+ "fsx" => "fsharp",
45
+ "gemfile" => "ruby",
46
+ "gemspec" => "ruby",
47
+ # glsl
48
+ # go
49
+ # groovy
50
+ "gvy" => "groovy",
51
+ "h" => "c",
52
+ "h++" => "cpp",
53
+ # haml
54
+ # handlebars
55
+ "hbr" => "handlebars",
56
+ "hh" => "cpp",
57
+ "hpp" => "cpp",
58
+ "hs" => "haskell",
59
+ "htm" => "markup",
60
+ "html" => "markup",
61
+ "hx" => "haxe",
62
+ "hxml" => "haxe",
63
+ "icn" => "icon",
64
+ "ijs" => "j",
65
+ # ini
66
+ "iol" => "jolie",
67
+ # java
68
+ "jl" => "julia",
69
+ "js" => "javascript",
70
+ # json
71
+ # jsx
72
+ "kt" => "kotlin",
73
+ "kts" => "kotlin",
74
+ # less
75
+ "lhs" => "haskell",
76
+ "lol" => "lolcode",
77
+ "lols" => "lolcode",
78
+ "ls" => "livescript",
79
+ # lua
80
+ "m" => "objective-c",
81
+ "mab" => "ruby",
82
+ # makefile
83
+ # markdown
84
+ "md" => "markdown",
85
+ # mel
86
+ "mkd" => "markdown",
87
+ "ml" => "ocaml",
88
+ "mli" => "ocaml",
89
+ "mm" => "objective-c",
90
+ # nim
91
+ # nix
92
+ "nsi" => "nsis",
93
+ "ol" => "jolie",
94
+ # oz
95
+ "pas" => "pascal",
96
+ "patch" => "diff",
97
+ "pde" => "processing",
98
+ "php" => %w[php php-extras],
99
+ "php3" => %w[php php-extras],
100
+ "php4" => %w[php php-extras],
101
+ "php5" => %w[php php-extras],
102
+ "phtml" => %w[php php-extras],
103
+ "pl" => "perl",
104
+ "pp" => "puppet",
105
+ "prawn" => "ruby",
106
+ "pro" => "prolog",
107
+ # properties
108
+ # pure
109
+ "py" => "python",
110
+ "py3" => "python",
111
+ "pyw" => "python",
112
+ "q" => "qore",
113
+ "qm" => "qore",
114
+ "qtest" => "qore",
115
+ # r
116
+ "rake" => "ruby",
117
+ "rakefile" => "ruby",
118
+ "rantfile" => "ruby",
119
+ "rb" => "ruby",
120
+ "rbw" => "ruby",
121
+ "rjs" => "ruby",
122
+ "rpdf" => "ruby",
123
+ "rs" => "rust",
124
+ "rst" => "rest",
125
+ "ru" => "ruby",
126
+ "rxml" => "ruby",
127
+ # sass
128
+ "sc" => "scala",
129
+ # scala
130
+ "scs" => "scheme",
131
+ # scss
132
+ "shader" => "glsl",
133
+ # sql
134
+ "ss" => "scheme",
135
+ "st" => "smalltalk",
136
+ "styl" => "stylus",
137
+ # swift
138
+ # tcl
139
+ "template" => "json",
140
+ "tex" => "latex",
141
+ # textile
142
+ "tmproj" => "markup",
143
+ "tpl" => "smarty",
144
+ "ts" => "typescript",
145
+ "v" => "verilog",
146
+ "vagrantfile" => "ruby",
147
+ "vhd" => "vhdl",
148
+ # vim
149
+ "xaml" => "markup",
150
+ "xhtml" => "markup",
151
+ "xml" => "markup",
152
+ # yaml
153
+ "yaws" => "erlang",
154
+ "yml" => "yaml",
155
+ ).freeze
156
+
157
+ class Location
158
+ CONTEXT_LINES = 2
159
+ MAX_LINES = 10
160
+
161
+ def initialize(source_buffer, location)
162
+ @source_buffer = source_buffer
163
+ @location = location
164
+ end
165
+
166
+ def begin_line
167
+ @begin_line ||= line("begin")
168
+ end
169
+
170
+ def end_line
171
+ @end_line ||= line("end")
172
+ end
173
+
174
+ def to_s
175
+ [
176
+ begin_line,
177
+ end_line,
178
+ ].uniq.join("-")
179
+ end
180
+
181
+ def start
182
+ [begin_line - CONTEXT_LINES, 1].max
183
+ end
184
+
185
+ def line_offset
186
+ start - 1
187
+ end
188
+
189
+ def code
190
+ first_line = start
191
+ last_line = [
192
+ end_line + CONTEXT_LINES,
193
+ begin_line + MAX_LINES + CONTEXT_LINES,
194
+ source_buffer.line_count,
195
+ ].min
196
+ source_buffer.source.lines[(first_line - 1)..(last_line - 1)].join("")
197
+ end
198
+
199
+ private
200
+
201
+ attr_reader :location, :source_buffer
202
+
203
+ def line(type)
204
+ if location["lines"]
205
+ location["lines"][type]
206
+ elsif location["positions"]
207
+ position_to_line(location["positions"][type])
208
+ end
209
+ end
210
+
211
+ def position_to_line(position)
212
+ if position["line"]
213
+ position["line"]
214
+ else
215
+ @source_buffer.decompose_position(position["offset"]).first
216
+ end
217
+ end
218
+ end
219
+
220
+ class SourceFile
221
+ def initialize(path, filesystem)
222
+ @path = path
223
+ @filesystem = filesystem
224
+ end
225
+
226
+ attr_reader :path
227
+
228
+ def syntaxes
229
+ ext = File.basename(path).split(".").last.downcase
230
+ Array(LANGUAGES[ext])
231
+ end
232
+
233
+ def code
234
+ filesystem.read_path(path)
235
+ end
236
+
237
+ def buffer
238
+ @buffer ||= SourceBuffer.new(path, code)
239
+ end
240
+
241
+ def location(loc)
242
+ Location.new(buffer, loc)
243
+ end
244
+
245
+ private
246
+
247
+ attr_reader :filesystem
248
+ end
249
+
250
+ class Issue
251
+ MARKDOWN_CONFIG = { autolink: true, fenced_code_blocks: true, no_intra_emphasis: true, tables: true }.freeze
252
+
253
+ def initialize(data, filesystem)
254
+ @data = data
255
+ @filesystem = filesystem
256
+ end
257
+
258
+ def description
259
+ data["description"]
260
+ end
261
+
262
+ def body
263
+ @body ||=
264
+ begin
265
+ text = data.fetch("content", {}).fetch("body", "").strip
266
+ unless text.empty?
267
+ markdown(text)
268
+ end
269
+ end
270
+ end
271
+
272
+ def source
273
+ @source ||= SourceFile.new(
274
+ data.fetch("location", {}).fetch("path", ""),
275
+ filesystem,
276
+ )
277
+ end
278
+
279
+ def location
280
+ @location ||=
281
+ Location.new(
282
+ source.buffer,
283
+ data["location"],
284
+ )
285
+ end
286
+
287
+ def other_locations
288
+ @other_locations ||=
289
+ begin
290
+ data.fetch("other_locations", []).map do |loc|
291
+ [SourceFile.new(loc["path"], filesystem), loc]
292
+ end.to_h
293
+ end
294
+ end
295
+
296
+ def categories
297
+ data.fetch("categories", [])
298
+ end
299
+
300
+ def engine_name
301
+ data["engine_name"]
302
+ end
303
+
304
+ private
305
+
306
+ attr_reader :data, :filesystem
307
+
308
+ def markdown(text)
309
+ html = Redcarpet::Render::HTML.new(
310
+ escape_html: false,
311
+ link_attributes: { target: "_blank" },
312
+ )
313
+ redcarpet = Redcarpet::Markdown.new(html, MARKDOWN_CONFIG)
314
+ redcarpet.render(text)
315
+ end
316
+ end
317
+
318
+ class IssueCollection
319
+ def initialize(filesystem)
320
+ @collection = []
321
+ @filesystem = filesystem
322
+ end
323
+
324
+ def each(&block)
325
+ collection.each(&block)
326
+ end
327
+
328
+ def <<(issue)
329
+ if issue.is_a? Hash
330
+ issue = Issue.new(issue, filesystem)
331
+ end
332
+ collection.push(issue)
333
+ end
334
+
335
+ def any?
336
+ collection.any?
337
+ end
338
+
339
+ def syntaxes
340
+ collection.flat_map do |issue|
341
+ issue.source.syntaxes
342
+ end.uniq.sort
343
+ end
344
+
345
+ def categories
346
+ collection.flat_map(&:categories).uniq.sort
347
+ end
348
+
349
+ def engines
350
+ collection.map(&:engine_name).uniq.compact.sort
351
+ end
352
+
353
+ private
354
+
355
+ attr_reader :collection, :filesystem
356
+ end
357
+
358
+ class ReportTemplate
359
+ include ERB::Util
360
+ attr_reader :issues
361
+
362
+ TEMPLATE_PATH = File.expand_path(File.join(File.dirname(__FILE__), "templates/html.erb"))
363
+
364
+ def initialize(issues, filesystem)
365
+ @issues = issues
366
+ @filesystem = filesystem
367
+ end
368
+
369
+ def render
370
+ template = File.read(TEMPLATE_PATH)
371
+ ERB.new(template, nil, "-").result(binding)
372
+ end
373
+
374
+ def project_name
375
+ File.basename(filesystem.root)
376
+ end
377
+
378
+ def param(str)
379
+ str.downcase.gsub(/\s+/, "-")
380
+ end
381
+
382
+ def params(values)
383
+ values.map { |c| param c }.join(" ")
384
+ end
385
+
386
+ private
387
+
388
+ attr_reader :filesystem
389
+ end
390
+
391
+ def finished
392
+ puts ReportTemplate.new(issues, @filesystem).render
393
+ end
394
+
395
+ def failed(_)
396
+ exit 1
397
+ end
398
+
399
+ private
400
+
401
+ def issues
402
+ @issues ||= IssueCollection.new(@filesystem)
403
+ end
404
+
405
+ def warnings
406
+ @warnings ||= []
407
+ end
408
+
409
+ def measurements
410
+ @measurements ||= []
411
+ end
412
+ end
413
+ end
414
+ end
415
+ end