Class: Herb::Project
Defined Under Namespace
Classes: ResultTracker
Constant Summary collapse
- TEMPLATE_ERRORS =
Known error types that indicate issues in the user’s template, not bugs in the parser.
[ "MissingOpeningTagError", "MissingClosingTagError", "TagNamesMismatchError", "VoidElementClosingTagError", "UnclosedElementError", "RubyParseError", "ERBControlFlowScopeError", "MissingERBEndTagError", "ERBMultipleBlocksInTagError", "ERBCaseWithConditionsError", "ConditionalElementMultipleTagsError", "ConditionalElementConditionMismatchError", "InvalidCommentClosingTagError", "OmittedClosingTagError", "UnclosedOpenTagError", "UnclosedCloseTagError", "UnclosedQuoteError", "MissingAttributeValueError", "UnclosedERBTagError", "StrayERBClosingTagError", "NestedERBTagError" ].freeze
- ISSUE_TYPES =
[ { key: :failed, label: "Parser crashed", symbol: "✗", color: :red, reportable: true, hint: "This could be a bug in the parser. Reporting it helps us improve Herb for everyone.", file_hint: ->(relative) { "Run `herb parse #{relative}` to see the parser output." } }, { key: :template_error, label: "Template errors", symbol: "✗", color: :red, hint: "These files have issues in the template. Review the errors and update your templates to fix them." }, { key: :unexpected_error, label: "Unexpected parse errors", symbol: "✗", color: :red, reportable: true, hint: "These errors may indicate a bug in the parser. Reporting them helps us make Herb more robust.", file_hint: ->(relative) { "Run `herb parse #{relative}` to see the parser output." } }, { key: :strict_parse_error, label: "Strict mode parse errors", symbol: "⚠", color: :yellow, hint: "These files use HTML patterns like omitted closing tags. Add explicit closing tags to fix." }, { key: :analyze_parse_error, label: "Analyze parse errors", symbol: "⚠", color: :yellow, hint: "These files have issues detected during analysis. Review the errors and update your templates." }, { key: :timeout, label: "Timed out", symbol: "⚠", color: :yellow, reportable: true, hint: "These files took too long to parse. This could indicate a parser issue. Reporting it helps us track down edge cases." }, { key: :validation_error, label: "Validation errors", symbol: "⚠", color: :yellow, hint: "These templates have security, nesting, or accessibility issues. The templates compile fine otherwise. Review and fix these to improve your template structure." }, { key: :compilation_failed, label: "Compilation errors", symbol: "✗", color: :red, reportable: true, hint: "These files could not be compiled to Ruby. This could be a bug in the engine. Reporting it helps us improve Herb's compatibility.", file_hint: ->(relative) { "Run `herb compile #{relative}` to see the compilation error." } }, { key: :strict_compilation_failed, label: "Strict mode compilation errors", symbol: "⚠", color: :yellow, hint: "These files fail to compile only in strict mode. Add explicit closing tags to fix, or pass --no-strict to allow.", file_hint: ->(relative) { "Run `herb compile #{relative}` to see the compilation error." } }, { key: :invalid_ruby, label: "Invalid Ruby output", symbol: "✗", color: :red, reportable: true, hint: "The engine produced Ruby code that doesn't parse. This is most likely a bug in the engine. Reporting it helps us fix it.", file_hint: ->(relative) { "Run `herb compile #{relative}` to see the compiled output." } } ].freeze
Constants included from Colors
Colors::CLEAR_SCREEN, Colors::HIDE_CURSOR, Colors::SHOW_CURSOR
Instance Attribute Summary collapse
-
#arena_stats ⇒ Object
Returns the value of attribute arena_stats.
-
#file_paths ⇒ Object
Returns the value of attribute file_paths.
-
#isolate ⇒ Object
Returns the value of attribute isolate.
-
#leak_check ⇒ Object
Returns the value of attribute leak_check.
-
#no_log_file ⇒ Object
Returns the value of attribute no_log_file.
-
#no_timing ⇒ Object
Returns the value of attribute no_timing.
-
#output_file ⇒ Object
Returns the value of attribute output_file.
-
#project_path ⇒ Object
Returns the value of attribute project_path.
-
#silent ⇒ Object
Returns the value of attribute silent.
-
#validate_ruby ⇒ Object
Returns the value of attribute validate_ruby.
-
#verbose ⇒ Object
Returns the value of attribute verbose.
Instance Method Summary collapse
- #absolute_path ⇒ Object
- #analyze! ⇒ Object
- #configuration ⇒ Object
- #exclude_patterns ⇒ Object
- #files ⇒ Object
- #include_patterns ⇒ Object
-
#initialize(project_path, output_file: nil) ⇒ Project
constructor
A new instance of Project.
- #print_file_report(file_path) ⇒ Object
Methods included from Colors
bold, bright_magenta, cyan, dimmed, enabled?, fg, fg_bg, green, magenta, red, white, yellow
Constructor Details
#initialize(project_path, output_file: nil) ⇒ Project
Returns a new instance of Project.
122 123 124 125 126 127 128 129 |
# File 'lib/herb/project.rb', line 122 def initialize(project_path, output_file: nil) @project_path = Pathname.new( project_path ? File.(".", project_path) : File.("../..", __dir__) ) date = Time.now.strftime("%Y-%m-%d_%H-%M-%S") @output_file = output_file || "#{date}_erb_parsing_result_#{@project_path.basename}.log" end |
Instance Attribute Details
#arena_stats ⇒ Object
Returns the value of attribute arena_stats.
15 16 17 |
# File 'lib/herb/project.rb', line 15 def arena_stats @arena_stats end |
#file_paths ⇒ Object
Returns the value of attribute file_paths.
15 16 17 |
# File 'lib/herb/project.rb', line 15 def file_paths @file_paths end |
#isolate ⇒ Object
Returns the value of attribute isolate.
15 16 17 |
# File 'lib/herb/project.rb', line 15 def isolate @isolate end |
#leak_check ⇒ Object
Returns the value of attribute leak_check.
15 16 17 |
# File 'lib/herb/project.rb', line 15 def leak_check @leak_check end |
#no_log_file ⇒ Object
Returns the value of attribute no_log_file.
15 16 17 |
# File 'lib/herb/project.rb', line 15 def no_log_file @no_log_file end |
#no_timing ⇒ Object
Returns the value of attribute no_timing.
15 16 17 |
# File 'lib/herb/project.rb', line 15 def no_timing @no_timing end |
#output_file ⇒ Object
Returns the value of attribute output_file.
15 16 17 |
# File 'lib/herb/project.rb', line 15 def output_file @output_file end |
#project_path ⇒ Object
Returns the value of attribute project_path.
15 16 17 |
# File 'lib/herb/project.rb', line 15 def project_path @project_path end |
#silent ⇒ Object
Returns the value of attribute silent.
15 16 17 |
# File 'lib/herb/project.rb', line 15 def silent @silent end |
#validate_ruby ⇒ Object
Returns the value of attribute validate_ruby.
15 16 17 |
# File 'lib/herb/project.rb', line 15 def validate_ruby @validate_ruby end |
#verbose ⇒ Object
Returns the value of attribute verbose.
15 16 17 |
# File 'lib/herb/project.rb', line 15 def verbose @verbose end |
Instance Method Details
#absolute_path ⇒ Object
143 144 145 |
# File 'lib/herb/project.rb', line 143 def absolute_path File.(@project_path, File.("../..", __dir__)) end |
#analyze! ⇒ Object
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
# File 'lib/herb/project.rb', line 168 def analyze! start_time = Time.now unless no_timing log = if no_log_file StringIO.new else File.open(output_file, "w") end begin log.puts heading("METADATA") log.puts "Herb Version: #{Herb.version}" log.puts "Reported at: #{Time.now.strftime("%Y-%m-%dT%H:%M:%S")}\n\n" log.puts heading("PROJECT") log.puts "Path: #{absolute_path}" log.puts "Config: #{configuration.config_path || "(defaults)"}" log.puts "Include: #{include_patterns.join(", ")}" log.puts "Exclude: #{exclude_patterns.join(", ")}\n\n" log.puts heading("PROCESSED FILES") if files.empty? = "No files found matching patterns: #{include_patterns.join(", ")}" log.puts puts return end @results = ResultTracker.new results = @results unless silent puts "" puts "#{bold("Herb")} 🌿 #{dimmed("v#{Herb::VERSION}")}" puts "" if configuration.config_path puts "#{green("✓")} Using Herb config file at #{dimmed(configuration.config_path)}" else puts dimmed("No .herb.yml found, using defaults") end puts dimmed("Analyzing #{files.count} #{pluralize(files.count, "file")}...") end finish_hook = lambda do |item, _index, _file_result| next if silent if verbose puts " #{relative_path(item)}" else print "." end end ensure_parallel! file_results = Parallel.map(files, in_processes: Parallel.processor_count, finish: finish_hook) do |file_path| process_file(file_path) end unless silent puts "" unless verbose puts "" puts separator end file_results.each do |result| merge_file_result(result, results, log) end log.puts "" duration = no_timing ? nil : Time.now - start_time print_file_lists(results, log) if results.problem_files.any? puts "\n #{separator}" print_issue_summary(results) if reportable_files?(results) puts "\n #{separator}" print_reportable_files(results) end end log_problem_file_details(results, log) if arena_stats print_arena_summary(file_results) end if leak_check print_leak_check_summary(file_results) end unless no_log_file puts "\n #{separator}" puts "\n #{dimmed("Results saved to #{output_file}")}" end puts "\n #{separator}" print_summary(results, log, duration) results.problem_files.any? ensure log.close unless no_log_file end end |
#configuration ⇒ Object
131 132 133 |
# File 'lib/herb/project.rb', line 131 def configuration @configuration ||= Configuration.load(@project_path.to_s) end |
#exclude_patterns ⇒ Object
139 140 141 |
# File 'lib/herb/project.rb', line 139 def exclude_patterns configuration.file_exclude_patterns end |
#files ⇒ Object
147 148 149 |
# File 'lib/herb/project.rb', line 147 def files @files ||= file_paths || find_files end |
#include_patterns ⇒ Object
135 136 137 |
# File 'lib/herb/project.rb', line 135 def include_patterns configuration.file_include_patterns end |
#print_file_report(file_path) ⇒ Object
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 |
# File 'lib/herb/project.rb', line 280 def print_file_report(file_path) file_path = File.(file_path) results = @results unless results puts "No results available. Run parse! first." return end relative = relative_path(file_path) issue_type = results.file_issue_type(file_path) unless issue_type puts "No issues found for #{relative}." return end diagnostics = results.file_diagnostics[file_path] file_content = results.file_contents[file_path] puts "- **Herb:** `#{Herb.version}`" puts "- **Ruby:** `#{RUBY_VERSION}`" puts "- **Platform:** `#{RUBY_PLATFORM}`" puts "- **Category:** `#{issue_type[:label]}`" if diagnostics&.any? puts "" puts "**Errors:**" diagnostics.each do |diagnostic| lines = diagnostic[:message].split("\n") puts "- **#{diagnostic[:name]}** #{lines.first}" lines.drop(1).each do |line| puts " #{line}" end end end if file_content puts "" puts "**Template:**" puts "```erb" puts file_content puts "```" end return unless issue_type[:key] == :invalid_ruby && file_content begin engine = Herb::Engine.new(file_content, filename: file_path, escape: true, validation_mode: :none) puts "" puts "**Compiled Ruby:**" puts "```ruby" puts engine.src puts "```" rescue StandardError # Skip if compilation fails entirely end end |