diff --git a/.rubocop.yml b/.rubocop.yml index 934b53771..21fc7229a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -48,6 +48,10 @@ Metrics/CyclomaticComplexity: Exclude: - lib/herb/project.rb - lib/herb/ast/nodes.rb + - lib/herb/range.rb + - lib/herb/parse_result.rb + - lib/herb/errors.rb + - lib/herb/cli.rb Metrics/MethodLength: Max: 20 @@ -55,26 +59,33 @@ Metrics/MethodLength: - lib/herb/ast/nodes.rb - lib/herb/cli.rb - lib/herb/project.rb + - lib/herb/errors.rb + - lib/herb/range.rb - templates/template.rb - - test/fork_helper.rb - - test/snapshot_utils.rb + - test/** Metrics/AbcSize: Exclude: - lib/herb/ast/node.rb - lib/herb/ast/nodes.rb + - lib/herb/lex_result.rb - lib/herb/cli.rb - lib/herb/errors.rb - lib/herb/project.rb + - lib/herb/parse_result.rb + - lib/herb/range.rb + - lib/herb/token.rb + - lib/herb/lint_result.rb - templates/template.rb - - test/fork_helper.rb - - test/snapshot_utils.rb + - test/** Metrics/ClassLength: Exclude: - lib/herb/cli.rb - lib/herb/project.rb - lib/herb/visitor.rb + - lib/herb/backend.rb + - lib/herb/ast/nodes.rb - test/**/*_test.rb Metrics/BlockLength: @@ -83,6 +94,7 @@ Metrics/BlockLength: - Rakefile - "*.gemspec" - "**/*.rake" + - lib/herb/cli.rb - lib/herb/project.rb - test/**/*_test.rb @@ -90,12 +102,17 @@ Metrics/ParameterLists: Exclude: - lib/herb/ast/nodes.rb - lib/herb/errors.rb + - lib/herb/lint_offense.rb Metrics/PerceivedComplexity: + Max: 15 Exclude: - lib/herb/ast/nodes.rb - lib/herb/cli.rb - lib/herb/project.rb + - lib/herb/range.rb + # - lib/herb/errors.rb + # - lib/herb/lex_result.rb - test/snapshot_utils.rb Layout/LineLength: @@ -104,6 +121,10 @@ Layout/LineLength: - test/**/*_test.rb - lib/herb/token.rb - lib/herb/ast/nodes.rb + - lib/herb/errors.rb + - lib/herb/backend.rb + - lib/herb/backends/native_backend.rb + - lib/herb/backends/node_backend.rb Layout/EmptyLines: Exclude: @@ -125,6 +146,18 @@ Layout/FirstHashElementIndentation: Layout/LeadingCommentSpace: Enabled: false +Layout/IndentationWidth: + Exclude: + - lib/herb/ast/nodes.rb + +Layout/ElseAlignment: + Exclude: + - lib/herb/ast/nodes.rb + +Layout/EndAlignment: + Exclude: + - lib/herb/ast/nodes.rb + Security/Eval: Exclude: - Rakefile diff --git a/Gemfile b/Gemfile index 629808ed2..79d0de407 100644 --- a/Gemfile +++ b/Gemfile @@ -9,6 +9,7 @@ gem "prism", github: "ruby/prism", tag: "v1.4.0" gem "lz_string" gem "maxitest" gem "minitest-difftastic", "~> 0.2" +gem "nodo", "~> 1.8" gem "rake", "~> 13.2" gem "rake-compiler", "~> 1.2" gem "rake-compiler-dock", "~> 1.9" diff --git a/Gemfile.lock b/Gemfile.lock index 6b12a48d6..b51d29a2b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -77,6 +77,8 @@ GEM minitest-difftastic (0.2.1) difftastic (~> 0.6) mutex_m (0.3.0) + nodo (1.8.1) + logger parallel (1.27.0) parser (3.3.8.0) ast (~> 2.4.1) @@ -159,6 +161,7 @@ DEPENDENCIES lz_string maxitest minitest-difftastic (~> 0.2) + nodo (~> 1.8) prism! rake (~> 13.2) rake-compiler (~> 1.2) diff --git a/Steepfile b/Steepfile index 40d550b4c..e081a8b7d 100644 --- a/Steepfile +++ b/Steepfile @@ -12,4 +12,5 @@ target :lib do ignore "lib/herb/libherb.rb" ignore "lib/herb/libherb" ignore "lib/herb/project.rb" + ignore "lib/herb/backends/node_backend.rb" end diff --git a/docs/docs/bindings/ruby/index.md b/docs/docs/bindings/ruby/index.md index 68ba75d7b..1598d1141 100644 --- a/docs/docs/bindings/ruby/index.md +++ b/docs/docs/bindings/ruby/index.md @@ -46,4 +46,72 @@ require "herb" ``` ::: -You are now ready to parse HTML+ERB in Ruby. +You are now ready to parse, format, lint, and print HTML+ERB in Ruby. See the [Quick Start](#quick-start) section below for basic usage. + +## Multi-Backend Architecture + +Herb for Ruby features a sophisticated multi-backend architecture that allows you to choose the best backend for your specific use case: + +- **Native Backend** (default) - Ruby C extension for parsing and lexing +- **Node Backend** - Uses Node.js packages for advanced features like formatting and linting +- **FFI Backend** - Foreign Function Interface backend (planned for future release) +- **WASM Backend** - WebAssembly backend (planned for future release) + +### Backend Selection + +Herb automatically selects the best available backend, but you can choose manually: + +```ruby +Herb.switch_backend(:native) # Native Ruby C-Extension Backend +Herb.switch_backend(:node) # Full feature set + +Herb.current_backend +# => "native" + +Herb.available_backends +# => [:native, :node] +``` + +### Cross-Backend Operations + +You can use different backends for different operations without switching: + + +```ruby +# Parse with native backend +result = Herb.parse(source) + +# Parse with Node backend +formatted = Herb.parse(source, backend: :node) + +# Switch to Node backend +Herb.switch_backend(:node) + +# Now uses Node backend for parsing +result = Herb.parse(source) +``` + +## Quick Start + +Once installed, you can start parsing, formatting, and linting HTML+ERB templates: + +:::code-group +```ruby +require "herb" + +# Parse HTML+ERB templates +result = Herb.parse('
Hello <%= user.name %>!
') + +# Format templates +formatted = Herb.format('
Messy
') + +# Lint templates for issues +lint_result = Herb.lint('') + +# Convert AST back to HTML +node = result.value +html_output = node.to_source +``` +::: + +For detailed examples and API reference, see the [Ruby Reference](/bindings/ruby/reference) documentation. diff --git a/docs/docs/bindings/ruby/reference.md b/docs/docs/bindings/ruby/reference.md index 59b062018..a9928905d 100644 --- a/docs/docs/bindings/ruby/reference.md +++ b/docs/docs/bindings/ruby/reference.md @@ -4,25 +4,43 @@ outline: deep # Ruby Reference -The `Herb` module exposes a few methods for you to lex, extract and parse HTML+ERB source code. +The `Herb` module provides a comprehensive API for lexing, parsing, formatting, linting, and printing HTML+ERB source code across multiple backend engines. -## Ruby API +## Core API Methods -`Herb` provides the following key methods: +### Parsing & Lexing +* `Herb.lex(source, backend: nil)` +* `Herb.lex_file(path, backend: nil)` +* `Herb.parse(source, backend: nil)` +* `Herb.parse_file(path, backend: nil)` -* `Herb.lex(source)` -* `Herb.lex_file(path)` -* `Herb.parse(source)` -* `Herb.parse_file(path)` +### Formatting & Linting +* `Herb.format(source, backend: :node)` +* `Herb.format_file(path, backend: :node)` +* `Herb.lint(source, backend: :node)` +* `Herb.lint_file(path, backend: :node)` + +### Code Extraction * `Herb.extract_ruby(source)` * `Herb.extract_html(source)` + +### Node Printing +* `Herb.print_node(node, backend: :node)` + +### Backend Management +* `Herb.switch_backend(backend_name)` +* `Herb.current_backend` +* `Herb.available_backends` +* `Herb.backend(backend_name)` + +### System Information * `Herb.version` ## Lexing The `Herb.lex` and `Herb.lex_file` methods allow you to tokenize an HTML document with embedded Ruby. -### `Herb.lex(source)` +### `Herb.lex(source, backend: nil)` :::code-group ```ruby @@ -38,7 +56,7 @@ Herb.lex(source).value ``` ::: -### `Herb.lex_file(path)` +### `Herb.lex_file(path, backend: nil)` :::code-group ```ruby @@ -60,11 +78,28 @@ Herb.lex_file("./index.html.erb").value The `Herb.parse` and `Herb.parse_file` methods allow you to parse an HTML document with embedded Ruby and returns you a parsed result of your document containing an Abstract Syntax Tree (AST) that you can use to structurally traverse the parsed document. -### `Herb.parse(source)` +### Basic Example :::code-group ```ruby -source = %(

Hello <%= user.name %>

) +require "herb" + +source = %(
Hello <%= user.name %>!
) +result = Herb.parse(source) + +if result.success? + puts result.value.class # => Herb::AST::DocumentNode +else + puts "Parse errors: #{result.errors}" +end +``` +::: + +### `Herb.parse(source, backend: nil)` + +:::code-group +```ruby +source = %(
Hello <%= user.name %>!
) Herb.parse(source).value # => @@ -100,7 +135,7 @@ Herb.parse(source).value ``` ::: -### `Herb.parse_file(path)` +### `Herb.parse_file(path, backend: nil)` :::code-group ```ruby @@ -167,13 +202,338 @@ result.visit(visitor) This allows you to analyze the parsed HTML+ERB programmatically. -## Metadata +## Formatting + +Herb provides advanced formatting capabilities through the Node backend using the `@herb-tools/formatter` package. + +> [!NOTE] Default Backend +> Format operations default to the Node backend since it's the only backend that supports formatting. You can override this by specifying a different backend. + +### Basic Example + +:::code-group +```ruby +require "herb" + +# Messy ERB template +messy_erb = '
Hello<%= user.name %>
' + +# Format with default options +formatted = Herb.format(messy_erb) +puts formatted +# => +#
+# Hello +# <%= user.name %> +#
+``` +::: + +### `Herb.format(source, backend: :node, **options)` + +:::code-group +```ruby +# Format messy ERB template +source = %(
Hello<%=user.name%>
<%if active%>
Active
<%else%>Inactive<%end%>) + +formatted = Herb.format(source) + +puts formatted +# => +#
+#
Hello<%= user.name %>
+#
+# +# <% if active %> +#
Active
+# <% else %> +# Inactive +# <% end %> +``` +::: + +### `Herb.format_file(path, backend: :node, **options)` + +:::code-group +```ruby +# Format an ERB file +formatted = Herb.format_file("./template.html.erb", backend: :node) +File.write("./formatted_template.html.erb", formatted) +``` +::: + +## Linting + +Herb provides comprehensive linting capabilities through the Node backend using the `@herb-tools/linter` package. + +> [!NOTE] Default Backend +> Lint operations default to the Node backend since it's the only backend that supports linting. You can override this by specifying a different backend. + +### Basic Example + +:::code-group +```ruby +require "herb" + +template = %(
Content
) + +lint_result = Herb.lint(template) + +if lint_result.success? + puts "✓ No lint issues found" +else + puts "Found #{lint_result.total_offenses} issues:" + + lint_result.error_offenses.each do |offense| + puts " Error: #{offense.message} (#{offense.rule})" + end + + lint_result.warning_offenses.each do |offense| + puts " Warning: #{offense.message} (#{offense.rule})" + end +end +``` +::: + +### `Herb.lint(source, backend: :node, **options)` + +:::code-group +```ruby +# Lint ERB template +source = %(
Content
) + +lint_result = Herb.lint(source) + +if lint_result.success? + puts "✓ No lint issues found" +else + puts "Found #{lint_result.total_offenses} issues:" + + lint_result.error_offenses.each do |offense| + puts " Error: #{offense.message} (#{offense.rule})" + end + + lint_result.warning_offenses.each do |offense| + puts " Warning: #{offense.message} (#{offense.rule})" + end +end +``` +::: + +### `Herb.lint_file(path, backend: :node, **options)` + +:::code-group +```ruby +# Lint an ERB file +lint_result = Herb.lint_file("./template.html.erb", backend: :node) + +# Generate a report +if !lint_result.clean? + puts lint_result.to_s + # => "✗ Found 3 lint offenses: 1 errors, 2 warnings" +end +``` +::: + +## Node Printing + +Convert AST nodes back to ERB/HTML strings with optional formatting. + +> [!NOTE] Default Backend +> Print operations default to the Node backend since it's the only backend that supports node printing. You can override this by specifying a different backend. + +### Basic Example + +:::code-group +```ruby +require "herb" + +source = %(
Hello <%= user.name %>!
) +result = Herb.parse(source) + +if result.success? + node = result.value + + formatted = node.to_source # Uses FormatPrinter + original = node.to_source(format: false) # Uses IdentityPrinter + + puts formatted +end +``` +::: + +### Node Methods + +Every AST node supports these methods: + +#### `node.to_source(backend: :node, format: true, **options)` + +:::code-group +```ruby +result = Herb.parse("
Hello <%= user.name %>!
") + +result.value.to_source +# => "
Hello <%= user.name %>!
" +``` +::: + +## Backend Management + +Herb supports multiple backend engines with different capabilities: + +### Available Backends + +- **Native Backend** - Ruby C extension (default) + - ✓ Parsing, Lexing, Code Extraction + - ✗ Formatting, Linting, Node Printing + +- **Node Backend** - Node.js integration with full feature set + - ✓ All features including formatting, linting, and printing + - Requires `nodo` gem and Node.js packages + +- **FFI Backend** - Foreign Function Interface (not yet implemented) + - 🚧 Planned for future release + - Will provide direct access to libherb via FFI + +- **WASM Backend** - WebAssembly (not yet implemented) + - 🚧 Planned for future release + - Will enable browser and sandboxed environments + +### Backend Selection + +#### `Herb.switch_backend(backend_name)` + +:::code-group +```ruby +# Switch to native backend (no dependencies) +Herb.switch_backend(:native) + +# Switch to node backend (all features) +Herb.switch_backend(:node) + +Herb.current_backend # => "node" +``` +::: + +#### `Herb.available_backends` + +:::code-group +```ruby +Herb.available_backends +# => [:native, :node] +``` +::: + +#### Cross-Backend Operations + +:::code-group +```ruby +# Keep native backend for fast parsing +Herb.switch_backend(:native) + +# Parse with current (native) backend +result = Herb.parse(source) + +# Format with Node backend without switching +formatted = Herb.format(source) + +# Lint with Node backend +lint_result = Herb.lint(source) + +# Print node +html_output = result.value.to_source(format: true) +html_output = result.value.to_source(format: false) + +Herb.current_backend # => "native" +``` +::: + +## LintResult API + +The `Herb::LintResult` class provides detailed information about linting results: + +### Properties + +:::code-group +```ruby +lint_result = Herb.lint(template, backend: :node) + +# Offense counts +lint_result.total_offenses # => 5 +lint_result.errors # => 2 +lint_result.warnings # => 2 +lint_result.infos # => 1 +lint_result.hints # => 0 + +# Status checks +lint_result.success? # => false (has errors) +lint_result.clean? # => false (has any offenses) + +# Access offenses by severity +lint_result.error_offenses # => [LintOffense, ...] +lint_result.warning_offenses # => [LintOffense, ...] +lint_result.info_offenses # => [LintOffense, ...] + +# Access offenses by rule +lint_result.offenses_for_rule("html-img-require-alt") # => [LintOffense, ...] +``` +::: + +### Output Formats + +:::code-group +```ruby +lint_result = Herb.lint(template, backend: :node) + +# String representation +puts lint_result.to_s +# => "✗ Found 3 lint offenses: 2 errors, 1 warnings" + +# Hash representation +hash = lint_result.to_h +# => { offenses: [...], errors: 2, warnings: 1, success: false } + +# JSON representation +json = lint_result.to_json +# => '{"offenses":[...],"errors":2,"warnings":1}' +``` +::: + +### LintOffense API + +Each lint offense provides detailed information: + +:::code-group +```ruby +offense = lint_result.error_offenses.first + +offense.message # => "Missing alt attribute" +offense.rule # => "html-img-require-alt" +offense.severity # => "error" +offense.code # => "E001" +offense.source # => "Herb Linter" + +# Location information +offense.location.start.line # => 1 +offense.location.start.column # => 5 +offense.location.end.line # => 1 +offense.location.end.column # => 20 + +# Severity checks +offense.error? # => true +offense.warning? # => false +offense.info? # => false +offense.hint? # => false +``` +::: + +## System Information ### `Herb.version` :::code-group ```ruby Herb.version -# => "herb gem v0.0.1, libherb v0.0.1 (Ruby C native extension)" +# => "Herb gem 0.6.0 - Herb::Backends::NativeBackend(native backend v0.6.0 via C extension)" ``` ::: diff --git a/ext/herb/extconf.rb b/ext/herb/extconf.rb index 9abcbfc27..7f4f8186c 100644 --- a/ext/herb/extconf.rb +++ b/ext/herb/extconf.rb @@ -55,6 +55,7 @@ core_src_files = %w[ extension.c + native_backend.c nodes.c error_helpers.c extension_helpers.c diff --git a/ext/herb/extension.c b/ext/herb/extension.c index e6e569e5e..01dd7810f 100644 --- a/ext/herb/extension.c +++ b/ext/herb/extension.c @@ -5,8 +5,6 @@ #include "extension_helpers.h" #include "nodes.h" -#include "../../src/include/analyze.h" - VALUE mHerb; VALUE cPosition; VALUE cLocation; @@ -16,126 +14,7 @@ VALUE cResult; VALUE cLexResult; VALUE cParseResult; -static VALUE Herb_lex(VALUE self, VALUE source) { - char* string = (char*) check_string(source); - - array_T* tokens = herb_lex(string); - - VALUE result = create_lex_result(tokens, source); - - herb_free_tokens(&tokens); - - return result; -} - -static VALUE Herb_lex_file(VALUE self, VALUE path) { - char* file_path = (char*) check_string(path); - array_T* tokens = herb_lex_file(file_path); - - VALUE source_value = read_file_to_ruby_string(file_path); - VALUE result = create_lex_result(tokens, source_value); - - herb_free_tokens(&tokens); - - return result; -} - -static VALUE Herb_parse(int argc, VALUE* argv, VALUE self) { - VALUE source, options; - rb_scan_args(argc, argv, "1:", &source, &options); - - char* string = (char*) check_string(source); - - parser_options_T* parser_options = NULL; - parser_options_T opts = { 0 }; - - if (!NIL_P(options)) { - VALUE track_whitespace = rb_hash_lookup(options, rb_str_new_cstr("track_whitespace")); - if (NIL_P(track_whitespace)) { track_whitespace = rb_hash_lookup(options, ID2SYM(rb_intern("track_whitespace"))); } - - if (!NIL_P(track_whitespace) && RTEST(track_whitespace)) { - opts.track_whitespace = true; - parser_options = &opts; - } - } - - AST_DOCUMENT_NODE_T* root = herb_parse(string, parser_options); - - herb_analyze_parse_tree(root, string); - - VALUE result = create_parse_result(root, source); - - ast_node_free((AST_NODE_T*) root); - - return result; -} - -static VALUE Herb_parse_file(VALUE self, VALUE path) { - char* file_path = (char*) check_string(path); - - VALUE source_value = read_file_to_ruby_string(file_path); - char* string = (char*) check_string(source_value); - - AST_DOCUMENT_NODE_T* root = herb_parse(string, NULL); - - VALUE result = create_parse_result(root, source_value); - - ast_node_free((AST_NODE_T*) root); - - return result; -} - -static VALUE Herb_lex_to_json(VALUE self, VALUE source) { - char* string = (char*) check_string(source); - buffer_T output; - - if (!buffer_init(&output)) { return Qnil; } - - herb_lex_json_to_buffer(string, &output); - - VALUE result = rb_str_new(output.value, output.length); - - buffer_free(&output); - - return result; -} - -static VALUE Herb_extract_ruby(VALUE self, VALUE source) { - char* string = (char*) check_string(source); - buffer_T output; - - if (!buffer_init(&output)) { return Qnil; } - - herb_extract_ruby_to_buffer(string, &output); - - VALUE result = rb_str_new_cstr(output.value); - buffer_free(&output); - - return result; -} - -static VALUE Herb_extract_html(VALUE self, VALUE source) { - char* string = (char*) check_string(source); - buffer_T output; - - if (!buffer_init(&output)) { return Qnil; } - - herb_extract_html_to_buffer(string, &output); - - VALUE result = rb_str_new_cstr(output.value); - buffer_free(&output); - - return result; -} - -static VALUE Herb_version(VALUE self) { - VALUE gem_version = rb_const_get(self, rb_intern("VERSION")); - VALUE libherb_version = rb_str_new_cstr(herb_version()); - VALUE libprism_version = rb_str_new_cstr(herb_prism_version()); - VALUE format_string = rb_str_new_cstr("herb gem v%s, libprism v%s, libherb v%s (Ruby C native extension)"); - - return rb_funcall(rb_mKernel, rb_intern("sprintf"), 4, format_string, gem_version, libprism_version, libherb_version); -} +void Init_native_backend(void); void Init_herb(void) { mHerb = rb_define_module("Herb"); @@ -147,12 +26,5 @@ void Init_herb(void) { cLexResult = rb_define_class_under(mHerb, "LexResult", cResult); cParseResult = rb_define_class_under(mHerb, "ParseResult", cResult); - rb_define_singleton_method(mHerb, "parse", Herb_parse, -1); - rb_define_singleton_method(mHerb, "lex", Herb_lex, 1); - rb_define_singleton_method(mHerb, "parse_file", Herb_parse_file, 1); - rb_define_singleton_method(mHerb, "lex_file", Herb_lex_file, 1); - rb_define_singleton_method(mHerb, "lex_to_json", Herb_lex_to_json, 1); - rb_define_singleton_method(mHerb, "extract_ruby", Herb_extract_ruby, 1); - rb_define_singleton_method(mHerb, "extract_html", Herb_extract_html, 1); - rb_define_singleton_method(mHerb, "version", Herb_version, 0); + Init_native_backend(); } diff --git a/ext/herb/native_backend.c b/ext/herb/native_backend.c new file mode 100644 index 000000000..908e46d40 --- /dev/null +++ b/ext/herb/native_backend.c @@ -0,0 +1,144 @@ +#include + +#include "error_helpers.h" +#include "extension.h" +#include "extension_helpers.h" +#include "nodes.h" + +#include "../../src/include/analyze.h" + +VALUE cNativeBackend; + +static VALUE NativeBackend_perform_lex(VALUE self, VALUE source) { + char* string = (char*) check_string(source); + + array_T* tokens = herb_lex(string); + + VALUE result = create_lex_result(tokens, source); + + herb_free_tokens(&tokens); + + return result; +} + +static VALUE NativeBackend_perform_lex_file(VALUE self, VALUE path) { + char* file_path = (char*) check_string(path); + array_T* tokens = herb_lex_file(file_path); + + VALUE source_value = read_file_to_ruby_string(file_path); + VALUE result = create_lex_result(tokens, source_value); + + herb_free_tokens(&tokens); + + return result; +} + +static VALUE NativeBackend_perform_parse(VALUE self, VALUE source, VALUE options) { + char* string = (char*) check_string(source); + + parser_options_T* parser_options = NULL; + parser_options_T opts = { 0 }; + + if (!NIL_P(options)) { + VALUE track_whitespace = rb_hash_lookup(options, rb_str_new_cstr("track_whitespace")); + if (NIL_P(track_whitespace)) { track_whitespace = rb_hash_lookup(options, ID2SYM(rb_intern("track_whitespace"))); } + + if (!NIL_P(track_whitespace) && RTEST(track_whitespace)) { + opts.track_whitespace = true; + parser_options = &opts; + } + } + + AST_DOCUMENT_NODE_T* root = herb_parse(string, parser_options); + + herb_analyze_parse_tree(root, string); + + VALUE result = create_parse_result(root, source); + + ast_node_free((AST_NODE_T*) root); + + return result; +} + +static VALUE NativeBackend_perform_parse_file(VALUE self, VALUE path, VALUE options) { + char* file_path = (char*) check_string(path); + + VALUE source_value = read_file_to_ruby_string(file_path); + char* string = (char*) check_string(source_value); + + parser_options_T* parser_options = NULL; + parser_options_T opts = { 0 }; + + if (!NIL_P(options)) { + VALUE track_whitespace = rb_hash_lookup(options, rb_str_new_cstr("track_whitespace")); + if (NIL_P(track_whitespace)) { track_whitespace = rb_hash_lookup(options, ID2SYM(rb_intern("track_whitespace"))); } + + if (!NIL_P(track_whitespace) && RTEST(track_whitespace)) { + opts.track_whitespace = true; + parser_options = &opts; + } + } + + AST_DOCUMENT_NODE_T* root = herb_parse(string, parser_options); + + herb_analyze_parse_tree(root, string); + + VALUE result = create_parse_result(root, source_value); + + ast_node_free((AST_NODE_T*) root); + + return result; +} + +static VALUE NativeBackend_perform_extract_ruby(VALUE self, VALUE source) { + char* string = (char*) check_string(source); + buffer_T output; + + if (!buffer_init(&output)) { return Qnil; } + + herb_extract_ruby_to_buffer(string, &output); + + VALUE result = rb_str_new_cstr(output.value); + buffer_free(&output); + + return result; +} + +static VALUE NativeBackend_perform_extract_html(VALUE self, VALUE source) { + char* string = (char*) check_string(source); + buffer_T output; + + if (!buffer_init(&output)) { return Qnil; } + + herb_extract_html_to_buffer(string, &output); + + VALUE result = rb_str_new_cstr(output.value); + buffer_free(&output); + + return result; +} + +static VALUE NativeBackend_backend_version(VALUE self) { + VALUE mHerb = rb_const_get(rb_cObject, rb_intern("Herb")); + VALUE gem_version = rb_const_get(mHerb, rb_intern("VERSION")); + VALUE libherb_version = rb_str_new_cstr(herb_version()); + VALUE libprism_version = rb_str_new_cstr(herb_prism_version()); + VALUE format_string = rb_str_new_cstr("herb gem v%s, native backend (libprism v%s, libherb v%s via C extension)"); + + return rb_funcall(rb_mKernel, rb_intern("sprintf"), 4, format_string, gem_version, libprism_version, libherb_version); +} + +void Init_native_backend(void) { + VALUE mHerb = rb_const_get(rb_cObject, rb_intern("Herb")); + VALUE mBackends = rb_const_get(mHerb, rb_intern("Backends")); + + cNativeBackend = rb_const_get(mBackends, rb_intern("NativeBackend")); + + rb_define_protected_method(cNativeBackend, "c_perform_lex", NativeBackend_perform_lex, 1); + rb_define_protected_method(cNativeBackend, "c_perform_lex_file", NativeBackend_perform_lex_file, 1); + rb_define_protected_method(cNativeBackend, "c_perform_parse", NativeBackend_perform_parse, 2); + rb_define_protected_method(cNativeBackend, "c_perform_parse_file", NativeBackend_perform_parse_file, 2); + rb_define_protected_method(cNativeBackend, "c_perform_extract_ruby", NativeBackend_perform_extract_ruby, 1); + rb_define_protected_method(cNativeBackend, "c_perform_extract_html", NativeBackend_perform_extract_html, 1); + rb_define_protected_method(cNativeBackend, "c_backend_version", NativeBackend_backend_version, 0); +} diff --git a/herb.gemspec b/herb.gemspec index a3445ac6e..577a72b85 100644 --- a/herb.gemspec +++ b/herb.gemspec @@ -45,6 +45,4 @@ Gem::Specification.new do |spec| spec.metadata["source_code_uri"] = "https://github.com/marcoroth/herb" spec.metadata["bug_tracker_uri"] = "https://github.com/marcoroth/herb/issues" spec.metadata["documentation_uri"] = "https://docs.herb-tools.dev" - - # spec.add_dependency "ffi" end diff --git a/lib/herb.rb b/lib/herb.rb index e558ef60a..932dce965 100644 --- a/lib/herb.rb +++ b/lib/herb.rb @@ -18,6 +18,9 @@ require_relative "herb/errors" require_relative "herb/warnings" +require_relative "herb/diagnostic" +require_relative "herb/lint_offense" +require_relative "herb/lint_result" require_relative "herb/cli" require_relative "herb/project" @@ -26,12 +29,110 @@ require_relative "herb/visitor" -begin - major, minor, _patch = RUBY_VERSION.split(".") #: [String, String, String] - require_relative "herb/#{major}.#{minor}/herb" -rescue LoadError - require_relative "herb/herb" -end +require_relative "herb/backend" +require_relative "herb/backend_loader" module Herb + class << self + def load_backend(backend_name = nil) + @backend = BackendLoader.load(backend_name) + self + end + + def backend_loaded? + @backend&.loaded? + end + + def ensure_backend! + load_backend unless backend_loaded? + end + + def lex(source) + ensure_backend! + @backend.lex(source) + end + + def lex_file(path) + ensure_backend! + @backend.lex_file(path) + end + + def parse(source, backend: nil, **options) + if backend + self.backend(backend).parse(source, options) + else + ensure_backend! + @backend.parse(source, options) + end + end + + def parse_file(path, backend: nil, **options) + if backend + self.backend(backend).parse_file(path, options) + else + ensure_backend! + @backend.parse_file(path, options) + end + end + + def extract_ruby(source) + ensure_backend! + @backend.extract_ruby(source) + end + + def extract_html(source) + ensure_backend! + @backend.extract_html(source) + end + + def version + ensure_backend! + @backend.version + end + + def format(source, backend: :node, **options) + self.backend(backend).format(source, options) + end + + def format_file(path, backend: :node, **options) + self.backend(backend).format_file(path, options) + end + + def lint(source, backend: :node, **options) + self.backend(backend).lint(source, options) + end + + def lint_file(path, backend: :node, **options) + self.backend(backend).lint_file(path, options) + end + + def print_node(node, backend: :node, **options) + self.backend(backend).print_node(node, options) + end + + def available_backends + BackendLoader.available_backends + end + + def switch_backend(backend_name) + @backend = BackendLoader.load(backend_name) + self + end + + def current_backend + @backend&.backend_name + end + + def backend(backend_name) + BackendLoader.load(backend_name) + end + end + + unless ENV["HERB_NO_AUTOLOAD"] + begin + Herb.load_backend + rescue StandardError + # Backend loading failed, but that's okay - methods will fail when called + end + end end diff --git a/lib/herb/ast/node.rb b/lib/herb/ast/node.rb index e1e5e4270..0cd7551ca 100644 --- a/lib/herb/ast/node.rb +++ b/lib/herb/ast/node.rb @@ -5,10 +5,10 @@ module Herb module AST class Node attr_reader :type #: String - attr_reader :location #: Location + attr_reader :location #: Location? attr_reader :errors #: Array[Herb::Errors::Error] - #: (String, Location, Array[Herb::Errors::Error]) -> void + #: (String, Location?, Array[Herb::Errors::Error]) -> void def initialize(type, location, errors = []) @type = type @location = location @@ -97,6 +97,11 @@ def compact_child_nodes def recursive_errors errors + compact_child_nodes.flat_map(&:recursive_errors) end + + #: (?backend: Symbol, **untyped) -> String + def to_source(backend: :node, format: true, **options) + Herb.backend(backend).print_node(self, { format: format }.merge(options)) + end end end end diff --git a/lib/herb/backend.rb b/lib/herb/backend.rb new file mode 100644 index 000000000..910cdb50d --- /dev/null +++ b/lib/herb/backend.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +module Herb + class Backend + attr_reader :loaded + + def initialize + @loaded = false + end + + def load + return self if @loaded + + perform_load + @loaded = true + self + end + + def loaded? + @loaded + end + + def lex(source) + ensure_loaded! + perform_lex(source) + end + + def lex_file(path) + ensure_loaded! + perform_lex_file(path) + end + + def parse(source, options = {}) + ensure_loaded! + perform_parse(source, options) + end + + def parse_file(path, options = {}) + ensure_loaded! + perform_parse_file(path, options) + end + + def extract_ruby(source) + ensure_loaded! + perform_extract_ruby(source) + end + + def extract_html(source) + ensure_loaded! + perform_extract_html(source) + end + + def version + ensure_loaded! + backend_version + end + + def format(source, options = {}) + ensure_loaded! + perform_format(source, options) + end + + def format_file(path, options = {}) + ensure_loaded! + perform_format_file(path, options) + end + + def lint(source, options = {}) + ensure_loaded! + perform_lint(source, options) + end + + def lint_file(path, options = {}) + ensure_loaded! + perform_lint_file(path, options) + end + + def print_node(node, options = {}) + ensure_loaded! + perform_print_node(node, options) + end + + def backend_name + self.class.name.split("::").last.sub(/Backend$/, "").downcase + end + + protected + + def perform_load + raise NotImplementedError, "#{self.class}#perform_load must be implemented" + end + + def perform_lex(source) + raise NotImplementedError, "#{self.class}#perform_lex must be implemented" + end + + def perform_lex_file(path) + raise NotImplementedError, "#{self.class}#perform_lex_file must be implemented" + end + + def perform_parse(source, options) + raise NotImplementedError, "#{self.class}#perform_parse must be implemented" + end + + def perform_parse_file(path, options) + raise NotImplementedError, "#{self.class}#perform_parse_file must be implemented" + end + + def perform_extract_ruby(source) + raise NotImplementedError, "#{self.class}#perform_extract_ruby must be implemented" + end + + def perform_extract_html(source) + raise NotImplementedError, "#{self.class}#perform_extract_html must be implemented" + end + + def backend_version + raise NotImplementedError, "#{self.class}#backend_version must be implemented" + end + + def perform_format(source, options) + raise NotImplementedError, "#{self.class}#perform_format not implemented. This backend does not support formatting." + end + + def perform_format_file(path, options) + source = File.read(path, encoding: "UTF-8") + perform_format(source, options) + end + + def perform_lint(source, options) + raise NotImplementedError, "#{self.class}#perform_lint not implemented. This backend does not support linting." + end + + def perform_lint_file(path, options) + source = File.read(path, encoding: "UTF-8") + perform_lint(source, options) + end + + def perform_print_node(node, options) + raise NotImplementedError, "#{self.class}#perform_print_node not implemented. This backend does not support node printing." + end + + private + + def ensure_loaded! + raise "Backend not loaded. Call #load first." unless loaded? + end + end +end diff --git a/lib/herb/backend_loader.rb b/lib/herb/backend_loader.rb new file mode 100644 index 000000000..a0cca0739 --- /dev/null +++ b/lib/herb/backend_loader.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module Herb + class BackendLoader + BACKENDS = { + native: "backends/native_backend", + node: "backends/node_backend", + }.freeze + + DEFAULT_PRIORITY = [:native, :node].freeze + + class << self + def load(backend_name = nil) + backend_name ||= detect_backend + + case backend_name.to_sym + when :native + load_native_backend + when :node + load_node_backend + else + raise ArgumentError, "Unknown backend: #{backend_name}. Available backends: #{BACKENDS.keys.join(", ")}" + end + end + + def detect_backend + if ENV["HERB_BACKEND"] + backend = ENV["HERB_BACKEND"]&.downcase&.to_sym + + return backend if BACKENDS.key?(backend) + + warn "Warning: Unknown HERB_BACKEND '#{ENV["HERB_BACKEND"]}'. Falling back to auto-detection." + end + + DEFAULT_PRIORITY.each do |backend_name| + backend = test_backend(backend_name) + return backend_name if backend + rescue LoadError, StandardError + # Try next backend + end + + raise "No suitable Herb backend found. Please install required dependencies." + end + + def available_backends + backends = [] #: Array[Herb::Backend] + + BACKENDS.each_key do |name| + backend = test_backend(name) + backends << name if backend + rescue LoadError, NotImplementedError, StandardError + # Backend not available, skip it + end + + backends + end + + private + + def load_native_backend + require_relative BACKENDS[:native] + backend = Herb::Backends::NativeBackend.new + backend.load + backend + rescue LoadError => e + raise LoadError, "Native backend not available. The C extension may not be compiled.\n#{e.message}" + end + + def load_node_backend + require_relative BACKENDS[:node] + backend = Herb::Backends::NodeBackend.new + backend.load + backend + rescue LoadError => e + raise LoadError, "Node backend not available. Please install the 'nodo' gem.\n#{e.message}" + end + + def test_backend(backend_name) + backend = case backend_name + when :native + require_relative BACKENDS[:native] + Herb::Backends::NativeBackend.new + when :node + require_relative BACKENDS[:node] + Herb::Backends::NodeBackend.new + end + + backend&.load + backend + rescue NotImplementedError + nil + end + end + end +end diff --git a/lib/herb/backends/ffi_backend.rb b/lib/herb/backends/ffi_backend.rb new file mode 100644 index 000000000..440dbc119 --- /dev/null +++ b/lib/herb/backends/ffi_backend.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require_relative "../backend" + +module Herb + module Backends + class FFIBackend < Backend + protected + + def perform_load + raise NotImplementedError, "FFI backend is not yet implemented. Please use the Native or Node backends." + end + + def perform_lex(source) + raise NotImplementedError, "FFI backend is not yet implemented. Please use the Native or Node backends." + end + + def perform_lex_file(path) + raise NotImplementedError, "FFI backend is not yet implemented. Please use the Native or Node backends." + end + + def perform_parse(source, options) + raise NotImplementedError, "FFI backend is not yet implemented. Please use the Native or Node backends." + end + + def perform_parse_file(path, options) + raise NotImplementedError, "FFI backend is not yet implemented. Please use the Native or Node backends." + end + + def perform_extract_ruby(source) + raise NotImplementedError, "FFI backend is not yet implemented. Please use the Native or Node backends." + end + + def perform_extract_html(source) + raise NotImplementedError, "FFI backend is not yet implemented. Please use the Native or Node backends." + end + + def backend_version + "ffi (not implemented)" + end + + def perform_format(source, options) + raise NotImplementedError, "FFI backend is not yet implemented. Please use the Node backend for formatting." + end + + def perform_lint(source, options) + raise NotImplementedError, "FFI backend is not yet implemented. Please use the Node backend for linting." + end + + def perform_print_node(node, options) + raise NotImplementedError, "FFI backend is not yet implemented. Please use the Node backend for printing." + end + end + end +end diff --git a/lib/herb/backends/native_backend.rb b/lib/herb/backends/native_backend.rb new file mode 100644 index 000000000..f493f537d --- /dev/null +++ b/lib/herb/backends/native_backend.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require_relative "../backend" + +module Herb + module Backends + class NativeBackend < Backend + protected + + def perform_load + major, minor, _patch = RUBY_VERSION.split(".") + + require_relative "../../herb/#{major}.#{minor}/herb" + rescue LoadError + begin + require_relative "../../herb/herb" + rescue LoadError + raise LoadError, "Native backend C extension not available. Please compile the extension with: bundle exec rake compile" + end + end + + def perform_lex(source) + raise NotImplementedError, "Native C extension not loaded" unless respond_to?(:c_perform_lex, true) + + c_perform_lex(source) + end + + def perform_lex_file(path) + raise NotImplementedError, "Native C extension not loaded" unless respond_to?(:c_perform_lex_file, true) + + c_perform_lex_file(path) + end + + def perform_parse(source, options) + raise NotImplementedError, "Native C extension not loaded" unless respond_to?(:c_perform_parse, true) + + c_perform_parse(source, options) + end + + def perform_parse_file(path, options) + raise NotImplementedError, "Native C extension not loaded" unless respond_to?(:c_perform_parse_file, true) + + c_perform_parse_file(path, options) + end + + def perform_extract_ruby(source) + raise NotImplementedError, "Native C extension not loaded" unless respond_to?(:c_perform_extract_ruby, true) + + c_perform_extract_ruby(source) + end + + def perform_extract_html(source) + raise NotImplementedError, "Native C extension not loaded" unless respond_to?(:c_perform_extract_html, true) + + c_perform_extract_html(source) + end + + def backend_version + if respond_to?(:c_backend_version, true) + c_backend_version + else + %(herb gem v#{Herb::VERSION}, native backend (C extension not loaded)) + end + end + + def perform_format(source, options) + raise NotImplementedError, "Formatting is not implemented in the native backend. Please use the Node backend: Herb.switch_backend(:node)" + end + + def perform_lint(source, options) + raise NotImplementedError, "Linting is not implemented in the native backend. Please use the Node backend: Herb.switch_backend(:node)" + end + + def perform_print_node(node, options) + raise NotImplementedError, "Node printing is not implemented in the native backend. Please use the Node backend: Herb.switch_backend(:node)" + end + end + end +end diff --git a/lib/herb/backends/node_backend.rb b/lib/herb/backends/node_backend.rb new file mode 100644 index 000000000..943ddc45f --- /dev/null +++ b/lib/herb/backends/node_backend.rb @@ -0,0 +1,237 @@ +# frozen_string_literal: true +# typed: ignore + +# rbs_inline: disabled + +require_relative "../backend" +require_relative "../lint_result" + +require "json" + +begin + require "nodo" +rescue LoadError + # Will be handled in perform_load +end + +module Herb + module Backends + class NodeBackend < Backend + attr_reader :nodo, :herb_node + + protected + + def perform_load + begin + require "nodo" + rescue LoadError + raise LoadError, "Node backend requires the 'nodo' gem. Please add it to your Gemfile or install it with: gem install nodo" + end + + @herb_node_class = Herb::Backends::HerbNode + @herb_node = @herb_node_class.new + end + + def perform_lex(source) + result_json = @herb_node.lex(source) + + parse_lex_result(result_json) + end + + def perform_lex_file(path) + result_json = @herb_node.lexFile(path) + + parse_lex_result(result_json) + end + + def perform_parse(source, options) + result_json = @herb_node.parse(source, convert_options(options)) + + parse_parse_result(result_json) + end + + def perform_parse_file(path, options) + result_json = @herb_node.parseFile(path, convert_options(options)) + + parse_parse_result(result_json) + end + + def perform_extract_ruby(source) + @herb_node.extractRuby(source) + end + + def perform_extract_html(source) + @herb_node.extractHTML(source) + end + + def perform_format(source, options) + @herb_node.format(source, convert_options(options)) + end + + def perform_lint(source, options) + result_json = @herb_node.lint(source, convert_options(options)) + + parse_lint_result(result_json) + end + + def perform_print_node(node, options) + node_json = node.to_json + + @herb_node.printNode(node_json, convert_options(options)) + end + + def backend_version + version = @herb_node.version + + %(herb gem v#{VERSION}, node backend (via nodo), #{version}) + end + + private + + def convert_options(options) + options.transform_keys { |key| key.to_s.gsub("_", "") } + end + + def parse_lex_result(json_string) + data = JSON.parse(json_string, symbolize_names: true) + + ::Herb::LexResult.from_hash(data) + end + + def parse_parse_result(json_string) + data = JSON.parse(json_string, symbolize_names: true) + + ::Herb::ParseResult.from_hash(data) + end + + def parse_lint_result(json_string) + data = JSON.parse(json_string, symbolize_names: true) + + ::Herb::LintResult.from_hash(data) + end + end + + # Only define HerbNode if Nodo is available + if defined?(Nodo) + class HerbNode < Nodo::Core + require Herb: "@herb-tools/node-wasm" + require HerbCore: "@herb-tools/core" + require HerbFormatter: "@herb-tools/formatter" + require HerbLinter: "@herb-tools/linter" + require HerbPrinter: "@herb-tools/printer" + + script <<~JS + let herbInstance = null; + + async function ensureLoaded() { + if (!herbInstance) { + herbInstance = await Herb.Herb.load(); + } + + return herbInstance; + } + JS + + function :lex, <<~JS + async (source) => { + const herb = await ensureLoaded(); + const result = herb.lex(source); + + return JSON.stringify(result); + } + JS + + function :lexFile, <<~JS + async (path) => { + const herb = await ensureLoaded(); + const result = herb.lexFile(path); + + return JSON.stringify(result); + } + JS + + function :parse, <<~JS + async (source, options = {}) => { + const herb = await ensureLoaded(); + const result = herb.parse(source, options); + + return JSON.stringify(result); + } + JS + + function :parseFile, <<~JS + async (path, options = {}) => { + const herb = await ensureLoaded(); + const result = herb.parseFile(path, options); + + return JSON.stringify(result); + } + JS + + function :extractRuby, <<~JS + async (source) => { + const herb = await ensureLoaded(); + + return herb.extractRuby(source); + } + JS + + function :extractHTML, <<~JS + async (source) => { + const herb = await ensureLoaded(); + + return herb.extractHTML(source); + } + JS + + function :version, <<~JS + async () => { + const herb = await ensureLoaded(); + + return herb.version; + } + JS + + function :format, <<~JS + async (source, options = {}) => { + const herb = await ensureLoaded(); + + return new HerbFormatter.Formatter(herb).format(source, options); + } + JS + + function :lint, <<~JS + async (source, options = {}) => { + const herb = await ensureLoaded(); + const result = new HerbLinter.Linter(herb).lint(source, options); + + return JSON.stringify(result); + } + JS + + function :printNode, <<~JS + async (json, options = {}) => { + const herb = await ensureLoaded(); + const serializedNode = JSON.parse(json); + + const node = HerbCore.fromSerializedNode(serializedNode); + + if (options.format === true) { + const defaultOptions = { + indentWidth: 2, + maxLineLength: 80, + ...options + }; + + const formatPrinter = new HerbFormatter.FormatPrinter("", defaultOptions); + + return formatPrinter.print(node); + } else { + return HerbPrinter.IdentityPrinter.print(node, options); + } + } + JS + end + end + end +end diff --git a/lib/herb/backends/wasm_backend.rb b/lib/herb/backends/wasm_backend.rb new file mode 100644 index 000000000..8462406b8 --- /dev/null +++ b/lib/herb/backends/wasm_backend.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require_relative "../backend" + +module Herb + module Backends + class WASMBackend < Backend + protected + + def perform_load + raise NotImplementedError, "WASM backend is not yet implemented. Please use the Native or Node backends." + end + + def perform_lex(source) + raise NotImplementedError, "WASM backend is not yet implemented. Please use the Native or Node backends." + end + + def perform_lex_file(path) + raise NotImplementedError, "WASM backend is not yet implemented. Please use the Native or Node backends." + end + + def perform_parse(source, options) + raise NotImplementedError, "WASM backend is not yet implemented. Please use the Native or Node backends." + end + + def perform_parse_file(path, options) + raise NotImplementedError, "WASM backend is not yet implemented. Please use the Native or Node backends." + end + + def perform_extract_ruby(source) + raise NotImplementedError, "WASM backend is not yet implemented. Please use the Native or Node backends." + end + + def perform_extract_html(source) + raise NotImplementedError, "WASM backend is not yet implemented. Please use the Native or Node backends." + end + + def backend_version + "wasm (not implemented)" + end + + def perform_format(source, options) + raise NotImplementedError, "WASM backend is not yet implemented. Please use the Node backend for formatting." + end + + def perform_lint(source, options) + raise NotImplementedError, "WASM backend is not yet implemented. Please use the Node backend for linting." + end + + def perform_print_node(node, options) + raise NotImplementedError, "WASM backend is not yet implemented. Please use the Node backend for printing." + end + end + end +end diff --git a/lib/herb/cli.rb b/lib/herb/cli.rb index 51ebc68dc..79ae68a5e 100644 --- a/lib/herb/cli.rb +++ b/lib/herb/cli.rb @@ -6,7 +6,8 @@ require "optparse" class Herb::CLI - attr_accessor :json, :silent, :no_interactive, :no_log_file, :no_timing, :local + attr_accessor :json, :silent, :no_interactive, :no_log_file, :no_timing, :local, :indent_width, + :max_line_length def initialize(args) @args = args @@ -86,6 +87,8 @@ def help(exit_code = 0) Commands: bundle exec herb lex [file] Lex a file. bundle exec herb parse [file] Parse a file. + bundle exec herb format [file] Format an ERB file with proper indentation and spacing. + bundle exec herb lint [file] Lint an ERB file for potential issues and style violations. bundle exec herb analyze [path] Analyze a project by passing a directory to the root of the project bundle exec herb ruby [file] Extract Ruby from a file. bundle exec herb html [file] Extract HTML from a file. @@ -111,11 +114,16 @@ def result project.no_log_file = no_log_file project.no_timing = no_timing has_issues = project.parse! + exit(has_issues ? 1 : 0) when "parse" Herb.parse(file_content) when "lex" Herb.lex(file_content) + when "format" + handle_format_command + when "lint" + handle_lint_command when "ruby" puts Herb.extract_ruby(file_content) exit(0) @@ -191,6 +199,15 @@ def option_parser parser.on("--local", "Use localhost for playground command instead of herb-tools.dev") do self.local = true end + + parser.on("-i", "--indent-width WIDTH", Integer, "Set indentation width for formatting (default: 2)") do |width| + self.indent_width = width + end + + parser.on("-l", "--max-line-length LENGTH", Integer, + "Set maximum line length for formatting (default: 80)") do |length| + self.max_line_length = length + end end end @@ -200,6 +217,62 @@ def options private + def handle_format_command + options = {} + options[:indentWidth] = indent_width if indent_width + options[:maxLineLength] = max_line_length if max_line_length + + formatted = Herb.format(file_content, **options) + puts formatted + exit(0) + rescue StandardError => e + puts "Error formatting file: #{e.message}" + exit(1) + end + + def handle_lint_command + lint_result = Herb.lint(file_content) + + if json + puts lint_result.to_json + elsif silent + exit(lint_result.success? ? 0 : 1) + else + puts lint_result + + unless lint_result.clean? + puts + puts "Issues found:" + + lint_result.error_offenses.each do |offense| + puts " ❌ Error: #{offense.message}" + puts " Rule: #{offense.rule}" + puts " Location: Line #{offense.location.start.line}, Column #{offense.location.start.column}" + puts + end + + lint_result.warning_offenses.each do |offense| + puts " ⚠️ Warning: #{offense.message}" + puts " Rule: #{offense.rule}" + puts " Location: Line #{offense.location.start.line}, Column #{offense.location.start.column}" + puts + end + + lint_result.info_offenses.each do |offense| + puts " ℹ️ Info: #{offense.message}" + puts " Rule: #{offense.rule}" + puts " Location: Line #{offense.location.start.line}, Column #{offense.location.start.column}" + puts + end + end + end + + exit(lint_result.success? ? 0 : 1) + rescue StandardError => e + puts "Error linting file: #{e.message}" + exit(1) + end + def print_version puts Herb.version exit(0) diff --git a/lib/herb/diagnostic.rb b/lib/herb/diagnostic.rb new file mode 100644 index 000000000..fa757b034 --- /dev/null +++ b/lib/herb/diagnostic.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true +# typed: true + +# rbs_inline: enabled + +module Herb + DIAGNOSTIC_SEVERITIES = %w[error warning info hint].freeze #: Array[String] + + class Diagnostic + attr_reader :message #: String + attr_reader :location #: Location? + attr_reader :severity #: String + attr_reader :code #: String? + attr_reader :source #: String? + + #: (message: String, location: Location?, severity: String, ?code: String?, ?source: String?) -> void + def initialize(message:, location:, severity:, code: nil, source: nil) + @message = message + @location = location + @severity = validate_severity(severity) + @code = code + @source = source + end + + #: () -> Hash[Symbol, untyped] + def to_h + { + message: @message, + location: @location.to_h, + severity: @severity, + code: @code, + source: @source, + }.compact + end + + #: (*untyped) -> String + def to_json(state = nil) + to_h.to_json(state) + end + + #: () -> String + def to_s + parts = [] #: Array[String] + parts << "[#{@severity.upcase}]" if @severity + parts << "[#{@code}]" if @code + parts << @message + parts << "at #{@location}" if @location + parts.join(" ") + end + + #: () -> bool + def error? + @severity == "error" + end + + #: () -> bool + def warning? + @severity == "warning" + end + + #: () -> bool + def info? + @severity == "info" + end + + #: () -> bool + def hint? + @severity == "hint" + end + + private + + #: (String) -> String + def validate_severity(severity) + unless DIAGNOSTIC_SEVERITIES.include?(severity) + raise ArgumentError, "Invalid severity '#{severity}'. Must be one of: #{DIAGNOSTIC_SEVERITIES.join(", ")}" + end + + severity + end + end +end diff --git a/lib/herb/lex_result.rb b/lib/herb/lex_result.rb index 5c32a76d1..844bbfe65 100644 --- a/lib/herb/lex_result.rb +++ b/lib/herb/lex_result.rb @@ -19,5 +19,19 @@ def success? def failed? errors.any? end + + #: (Hash[untyped, untyped]) -> LexResult + def self.from_hash(data) + tokens_data = data[:value] || data["value"] || [] + warnings_data = data[:warnings] || data["warnings"] || [] + errors_data = data[:errors] || data["errors"] || [] + source = data[:source] || data["source"] || "" + + tokens = tokens_data.map { |token_data| Token.from_hash(token_data) } + warnings = warnings_data.map { |warning_data| Herb::Warnings::Warning.from_hash(warning_data) } + errors = errors_data.map { |error_data| Herb::Errors::Error.from_hash(error_data) } + + new(tokens, source, warnings, errors) + end end end diff --git a/lib/herb/lint_offense.rb b/lib/herb/lint_offense.rb new file mode 100644 index 000000000..d63b942fb --- /dev/null +++ b/lib/herb/lint_offense.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true +# typed: true + +# rbs_inline: enabled + +require_relative "diagnostic" + +module Herb + class LintOffense < Diagnostic + attr_reader :rule #: String + + #: (message: String, location: Location?, severity: String, rule: String, ?code: String?, ?source: String?) -> void + def initialize(message:, location:, severity:, rule:, code: nil, source: "linter") + super( + message: message, + location: location, + severity: severity, + code: code, + source: source + ) + @rule = rule + end + + #: () -> Hash[Symbol, untyped] + def to_h + super.merge(rule: @rule) + end + + #: () -> String + def to_s + parts = [] #: Array[String] + + parts << "[#{@severity.upcase}]" if @severity + parts << "[#{@rule}]" if @rule + parts << "[#{@code}]" if @code + parts << @message + parts << "at #{@location}" if @location + + parts.join(" ") + end + + #: (Hash[untyped, untyped]) -> LintOffense + def self.from_hash(offense_data) + new( + message: offense_data[:message] || "Unknown lint offense", + location: Location.from_hash(offense_data[:location]), + severity: offense_data[:severity] || "error", + rule: offense_data[:rule] || "unknown", + code: offense_data[:code], + source: offense_data[:source] || "linter" + ) + end + end +end diff --git a/lib/herb/lint_result.rb b/lib/herb/lint_result.rb new file mode 100644 index 000000000..f5ab1a65c --- /dev/null +++ b/lib/herb/lint_result.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true +# typed: true + +# rbs_inline: enabled + +require_relative "lint_offense" + +module Herb + class LintResult + attr_reader :offenses #: Array[LintOffense] + + #: (?Array[LintOffense | Hash[untyped, untyped]]) -> void + def initialize(offenses = []) + @offenses = offenses.map { |offense| + offense.is_a?(LintOffense) ? offense : LintOffense.from_hash(offense) + } + end + + #: () -> Integer + def errors + @offenses.count(&:error?) + end + + #: () -> Integer + def warnings + @offenses.count(&:warning?) + end + + #: () -> Integer + def infos + @offenses.count(&:info?) + end + + #: () -> Integer + def hints + @offenses.count(&:hint?) + end + + #: () -> Integer + def total_offenses + @offenses.length + end + + #: () -> bool + def success? + errors.zero? + end + + #: () -> bool + def clean? + @offenses.empty? + end + + #: (String) -> Array[LintOffense] + def offenses_by_severity(severity) + @offenses.select { |offense| offense.severity == severity } + end + + #: () -> Array[LintOffense] + def error_offenses + offenses_by_severity("error") + end + + #: () -> Array[LintOffense] + def warning_offenses + offenses_by_severity("warning") + end + + #: () -> Array[LintOffense] + def info_offenses + offenses_by_severity("info") + end + + #: () -> Array[LintOffense] + def hint_offenses + offenses_by_severity("hint") + end + + #: (String) -> Array[LintOffense] + def offenses_for_rule(rule_name) + @offenses.select { |offense| offense.rule == rule_name } + end + + #: () -> Hash[Symbol, untyped] + def to_h + { + offenses: @offenses.map(&:to_h), + errors: errors, + warnings: warnings, + infos: infos, + hints: hints, + total_offenses: total_offenses, + success: success?, + clean: clean?, + } + end + + #: (?untyped) -> String + def to_json(state = nil) + to_h.to_json(state) + end + + #: () -> String + def to_s + return "✓ No lint offenses found" if clean? + + parts = [] #: Array[String] + parts << "#{errors} errors" if errors.positive? + parts << "#{warnings} warnings" if warnings.positive? + parts << "#{infos} infos" if infos.positive? + parts << "#{hints} hints" if hints.positive? + + "✗ Found #{total_offenses} lint offenses: #{parts.join(", ")}" + end + + #: (Hash[untyped, untyped]) -> LintResult + def self.from_hash(hash_result) + offenses = (hash_result[:offenses] || hash_result["offenses"] || []).map do |offense_data| + LintOffense.from_hash(offense_data) + end + + new(offenses) + end + end +end diff --git a/lib/herb/location.rb b/lib/herb/location.rb index e7c4a1a10..9c921fba5 100644 --- a/lib/herb/location.rb +++ b/lib/herb/location.rb @@ -25,14 +25,34 @@ def self.[](start_line, start_column, end_line, end_column) from(start_line, start_column, end_line, end_column) end + #: (Hash[untyped, untyped]|nil) -> Location? + def self.from_hash(hash_data) + return nil if hash_data.nil? + + start_data = hash_data[:start] || hash_data["start"] + end_data = hash_data[:end] || hash_data["end"] + + start_position = Position.from_hash(start_data) + end_position = Position.from_hash(end_data) + + return nil if start_position.nil? || end_position.nil? + + new(start_position, end_position) + end + #: () -> serialized_location def to_hash { - start: start, - end: self.end, + start: start&.to_hash, + end: self.end&.to_hash, } #: Herb::serialized_location end + #: () -> serialized_location + def to_h + to_hash + end + #: (?untyped) -> String def to_json(state = nil) to_hash.to_json(state) diff --git a/lib/herb/parse_result.rb b/lib/herb/parse_result.rb index 9b0bc1764..37831e945 100644 --- a/lib/herb/parse_result.rb +++ b/lib/herb/parse_result.rb @@ -32,9 +32,30 @@ def pretty_errors JSON.pretty_generate(errors) end + #: () -> String + def to_source(...) + value.to_source(...) + end + #: (Visitor) -> void def visit(visitor) value.accept(visitor) end + + #: (Hash[untyped, untyped]) -> ParseResult + def self.from_hash(data) + value_date = data[:value] || data["value"] + warnings_data = data[:warnings] || data["warnings"] || [] #: Array[untyped] + errors_data = data[:errors] || data["errors"] || [] #: Array[untyped] + source = data[:source] || data["source"] || "" + + value = value_date ? Herb::AST::Node.node_from_hash(value_date) : nil + raise "Missing document node" unless value.is_a?(Herb::AST::DocumentNode) + + warnings = warnings_data.map { |warning_data| Herb::Warnings::Warning.from_hash(warning_data) } + errors = errors_data.map { |error_data| Herb::Errors::Error.from_hash(error_data) } + + new(value, source, warnings, errors) + end end end diff --git a/lib/herb/position.rb b/lib/herb/position.rb index f7131d151..b919a5eac 100644 --- a/lib/herb/position.rb +++ b/lib/herb/position.rb @@ -22,11 +22,37 @@ def self.from(line, column) new(line, column) end + #: (Hash[untyped, untyped]|nil) -> Position? + def self.from_hash(hash_data) + return nil if hash_data.nil? + + line = hash_data[:line] || hash_data["line"] || 1 + column = hash_data[:column] || hash_data["column"] || 0 + + line = if line.is_a?(Integer) + line + else + (line.respond_to?(:to_i) ? line.to_i : 1) + end + column = if column.is_a?(Integer) + column + else + (column.respond_to?(:to_i) ? column.to_i : 0) + end + + new(line, column) + end + #: () -> serialized_position def to_hash { line: line, column: column } #: Herb::serialized_position end + #: () -> serialized_position + def to_h + to_hash + end + #: (?untyped) -> String def to_json(state = nil) to_hash.to_json(state) diff --git a/lib/herb/range.rb b/lib/herb/range.rb index 7adb16c10..a75b64e2e 100644 --- a/lib/herb/range.rb +++ b/lib/herb/range.rb @@ -22,6 +22,43 @@ def self.from(from, to) new(from, to) end + #: (Array[untyped]|Hash[untyped, untyped]|nil) -> Range? + def self.from_hash(data) + return nil if data.nil? + + case data + when Array + from = if data[0].is_a?(Integer) + data[0] + else + (data[0].respond_to?(:to_i) ? data[0].to_i : 0) + end + to = if data[1].is_a?(Integer) + data[1] + else + (data[1].respond_to?(:to_i) ? data[1].to_i : 0) + end + + new(from, to) + when Hash + from_value = data[:start] || data["start"] || data[:from] || data["from"] || 0 + to_value = data[:end] || data["end"] || data[:to] || data["to"] || 0 + + from = if from_value.is_a?(Integer) + from_value + else + (from_value.respond_to?(:to_i) ? from_value.to_i : 0) + end + to = if to_value.is_a?(Integer) + to_value + else + (to_value.respond_to?(:to_i) ? to_value.to_i : 0) + end + + new(from, to) + end + end + #: () -> serialized_range def to_a [from, to] #: Herb::serialized_range diff --git a/lib/herb/token.rb b/lib/herb/token.rb index e12d3c4d4..ba16cba3c 100644 --- a/lib/herb/token.rb +++ b/lib/herb/token.rb @@ -26,6 +26,21 @@ def to_hash } #: Herb::serialized_token end + #: (Hash[untyped, untyped]?) -> Token? + def self.from_hash(data) + return if data.nil? + + value = data[:value] || data["value"] || "" + type = data[:type] || data["type"] || "" + + range = Herb::Range.from_hash(data[:range] || data["range"]) + location = Location.from_hash(data[:location] || data["location"]) + + return nil if range.nil? || location.nil? + + new(value, range, location, type) + end + #: (?untyped) -> String def to_json(state = nil) to_hash.to_json(state) diff --git a/lib/herb/warnings.rb b/lib/herb/warnings.rb index 9020a3d7d..a74b60090 100644 --- a/lib/herb/warnings.rb +++ b/lib/herb/warnings.rb @@ -5,10 +5,10 @@ module Herb module Warnings class Warning attr_reader :type #: String - attr_reader :location #: Location + attr_reader :location #: Location? attr_reader :message #: String - #: (String, Location, String) -> void + #: (String, Location?, String) -> void def initialize(type, location, message) @type = type @location = location @@ -24,6 +24,15 @@ def to_hash } end + #: (Hash[untyped, untyped]) -> Warning + def self.from_hash(data) + type = data[:type] || data["type"] || "" + location = Location.from_hash(data[:location] || data["location"]) + message = data[:message] || data["message"] || "" + + new(type, location, message) + end + #: () -> String def class_name self.class.name || "Warning" diff --git a/sig/herb.rbs b/sig/herb.rbs index 4195b972f..163167f03 100644 --- a/sig/herb.rbs +++ b/sig/herb.rbs @@ -1,4 +1,41 @@ # Generated from lib/herb.rb with RBS::Inline module Herb + def self.load_backend: (?untyped backend_name) -> untyped + + def self.backend_loaded?: () -> untyped + + def self.ensure_backend!: () -> untyped + + def self.lex: (untyped source) -> untyped + + def self.lex_file: (untyped path) -> untyped + + def self.parse: (untyped source, ?backend: untyped, **untyped options) -> untyped + + def self.parse_file: (untyped path, ?backend: untyped, **untyped options) -> untyped + + def self.extract_ruby: (untyped source) -> untyped + + def self.extract_html: (untyped source) -> untyped + + def self.version: () -> untyped + + def self.format: (untyped source, ?backend: untyped, **untyped options) -> untyped + + def self.format_file: (untyped path, ?backend: untyped, **untyped options) -> untyped + + def self.lint: (untyped source, ?backend: untyped, **untyped options) -> untyped + + def self.lint_file: (untyped path, ?backend: untyped, **untyped options) -> untyped + + def self.print_node: (untyped node, ?backend: untyped, **untyped options) -> untyped + + def self.available_backends: () -> untyped + + def self.switch_backend: (untyped backend_name) -> untyped + + def self.current_backend: () -> untyped + + def self.backend: (untyped backend_name) -> untyped end diff --git a/sig/herb/ast/node.rbs b/sig/herb/ast/node.rbs index c89780470..30518e7f1 100644 --- a/sig/herb/ast/node.rbs +++ b/sig/herb/ast/node.rbs @@ -5,12 +5,12 @@ module Herb class Node attr_reader type: String - attr_reader location: Location + attr_reader location: Location? attr_reader errors: Array[Herb::Errors::Error] - # : (String, Location, Array[Herb::Errors::Error]) -> void - def initialize: (String, Location, Array[Herb::Errors::Error]) -> void + # : (String, Location?, Array[Herb::Errors::Error]) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error]) -> void # : () -> serialized_node def to_hash: () -> serialized_node @@ -46,6 +46,9 @@ module Herb # : () -> Array[Herb::Errors::Error] def recursive_errors: () -> Array[Herb::Errors::Error] + + # : (?backend: Symbol, **untyped) -> String + def to_source: (?backend: Symbol, **untyped) -> String end end end diff --git a/sig/herb/ast/nodes.rbs b/sig/herb/ast/nodes.rbs index fc211744b..136f2dc38 100644 --- a/sig/herb/ast/nodes.rbs +++ b/sig/herb/ast/nodes.rbs @@ -2,15 +2,24 @@ module Herb module AST + # Dynamically reopen Node class to add node_from_hash method with knowledge of all node types + class Node + # : (Hash[untyped, untyped]?) -> Node? + def self.node_from_hash: (Hash[untyped, untyped]?) -> Node? + end + class DocumentNode < Node attr_reader children: Array[Herb::AST::Node] - # : (String, Location, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void + # : (String, Location?, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void # : () -> serialized_document_node def to_hash: () -> serialized_document_node + # : (Hash[untyped, untyped]) -> DocumentNode + def self.from_hash: (Hash[untyped, untyped]) -> DocumentNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -30,12 +39,15 @@ module Herb class LiteralNode < Node attr_reader content: String - # : (String, Location, Array[Herb::Errors::Error], String) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], String) -> void + # : (String, Location?, Array[Herb::Errors::Error], String) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], String) -> void # : () -> serialized_literal_node def to_hash: () -> serialized_literal_node + # : (Hash[untyped, untyped]) -> LiteralNode + def self.from_hash: (Hash[untyped, untyped]) -> LiteralNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -53,22 +65,25 @@ module Herb end class HTMLOpenTagNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader tag_name: Herb::Token + attr_reader tag_name: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? attr_reader children: Array[Herb::AST::Node] attr_reader is_void: bool - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], bool) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], bool) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], bool) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], bool) -> void # : () -> serialized_html_open_tag_node def to_hash: () -> serialized_html_open_tag_node + # : (Hash[untyped, untyped]) -> HTMLOpenTagNode + def self.from_hash: (Hash[untyped, untyped]) -> HTMLOpenTagNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -86,20 +101,23 @@ module Herb end class HTMLCloseTagNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader tag_name: Herb::Token + attr_reader tag_name: Herb::Token? attr_reader children: Array[Herb::AST::Node] - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void # : () -> serialized_html_close_tag_node def to_hash: () -> serialized_html_close_tag_node + # : (Hash[untyped, untyped]) -> HTMLCloseTagNode + def self.from_hash: (Hash[untyped, untyped]) -> HTMLCloseTagNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -117,22 +135,25 @@ module Herb end class HTMLElementNode < Node - attr_reader open_tag: Herb::AST::HTMLOpenTagNode + attr_reader open_tag: Herb::AST::HTMLOpenTagNode? - attr_reader tag_name: Herb::Token + attr_reader tag_name: Herb::Token? attr_reader body: Array[Herb::AST::Node] - attr_reader close_tag: Herb::AST::HTMLCloseTagNode + attr_reader close_tag: Herb::AST::HTMLCloseTagNode? attr_reader is_void: bool - # : (String, Location, Array[Herb::Errors::Error], Herb::AST::HTMLOpenTagNode, Herb::Token, Array[Herb::AST::Node], Herb::AST::HTMLCloseTagNode, bool) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::AST::HTMLOpenTagNode, Herb::Token, Array[Herb::AST::Node], Herb::AST::HTMLCloseTagNode, bool) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::AST::HTMLOpenTagNode?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::HTMLCloseTagNode?, bool) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::AST::HTMLOpenTagNode?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::HTMLCloseTagNode?, bool) -> void # : () -> serialized_html_element_node def to_hash: () -> serialized_html_element_node + # : (Hash[untyped, untyped]) -> HTMLElementNode + def self.from_hash: (Hash[untyped, untyped]) -> HTMLElementNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -150,20 +171,23 @@ module Herb end class HTMLAttributeValueNode < Node - attr_reader open_quote: Herb::Token + attr_reader open_quote: Herb::Token? attr_reader children: Array[Herb::AST::Node] - attr_reader close_quote: Herb::Token + attr_reader close_quote: Herb::Token? attr_reader quoted: bool - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token, bool) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token, bool) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?, bool) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?, bool) -> void # : () -> serialized_html_attribute_value_node def to_hash: () -> serialized_html_attribute_value_node + # : (Hash[untyped, untyped]) -> HTMLAttributeValueNode + def self.from_hash: (Hash[untyped, untyped]) -> HTMLAttributeValueNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -183,12 +207,15 @@ module Herb class HTMLAttributeNameNode < Node attr_reader children: Array[Herb::AST::Node] - # : (String, Location, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void + # : (String, Location?, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Array[Herb::AST::Node]) -> void # : () -> serialized_html_attribute_name_node def to_hash: () -> serialized_html_attribute_name_node + # : (Hash[untyped, untyped]) -> HTMLAttributeNameNode + def self.from_hash: (Hash[untyped, untyped]) -> HTMLAttributeNameNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -206,18 +233,21 @@ module Herb end class HTMLAttributeNode < Node - attr_reader name: Herb::AST::HTMLAttributeNameNode + attr_reader name: Herb::AST::HTMLAttributeNameNode? - attr_reader equals: Herb::Token + attr_reader equals: Herb::Token? - attr_reader value: Herb::AST::HTMLAttributeValueNode + attr_reader value: Herb::AST::HTMLAttributeValueNode? - # : (String, Location, Array[Herb::Errors::Error], Herb::AST::HTMLAttributeNameNode, Herb::Token, Herb::AST::HTMLAttributeValueNode) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::AST::HTMLAttributeNameNode, Herb::Token, Herb::AST::HTMLAttributeValueNode) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::AST::HTMLAttributeNameNode?, Herb::Token?, Herb::AST::HTMLAttributeValueNode?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::AST::HTMLAttributeNameNode?, Herb::Token?, Herb::AST::HTMLAttributeValueNode?) -> void # : () -> serialized_html_attribute_node def to_hash: () -> serialized_html_attribute_node + # : (Hash[untyped, untyped]) -> HTMLAttributeNode + def self.from_hash: (Hash[untyped, untyped]) -> HTMLAttributeNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -237,12 +267,15 @@ module Herb class HTMLTextNode < Node attr_reader content: String - # : (String, Location, Array[Herb::Errors::Error], String) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], String) -> void + # : (String, Location?, Array[Herb::Errors::Error], String) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], String) -> void # : () -> serialized_html_text_node def to_hash: () -> serialized_html_text_node + # : (Hash[untyped, untyped]) -> HTMLTextNode + def self.from_hash: (Hash[untyped, untyped]) -> HTMLTextNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -260,18 +293,21 @@ module Herb end class HTMLCommentNode < Node - attr_reader comment_start: Herb::Token + attr_reader comment_start: Herb::Token? attr_reader children: Array[Herb::AST::Node] - attr_reader comment_end: Herb::Token + attr_reader comment_end: Herb::Token? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void # : () -> serialized_html_comment_node def to_hash: () -> serialized_html_comment_node + # : (Hash[untyped, untyped]) -> HTMLCommentNode + def self.from_hash: (Hash[untyped, untyped]) -> HTMLCommentNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -289,18 +325,21 @@ module Herb end class HTMLDoctypeNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? attr_reader children: Array[Herb::AST::Node] - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void # : () -> serialized_html_doctype_node def to_hash: () -> serialized_html_doctype_node + # : (Hash[untyped, untyped]) -> HTMLDoctypeNode + def self.from_hash: (Hash[untyped, untyped]) -> HTMLDoctypeNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -318,18 +357,21 @@ module Herb end class XMLDeclarationNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? attr_reader children: Array[Herb::AST::Node] - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void # : () -> serialized_xml_declaration_node def to_hash: () -> serialized_xml_declaration_node + # : (Hash[untyped, untyped]) -> XMLDeclarationNode + def self.from_hash: (Hash[untyped, untyped]) -> XMLDeclarationNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -347,18 +389,21 @@ module Herb end class CDATANode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? attr_reader children: Array[Herb::AST::Node] - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Array[Herb::AST::Node], Herb::Token) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Array[Herb::AST::Node], Herb::Token?) -> void # : () -> serialized_cdata_node def to_hash: () -> serialized_cdata_node + # : (Hash[untyped, untyped]) -> CDATANode + def self.from_hash: (Hash[untyped, untyped]) -> CDATANode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -376,14 +421,17 @@ module Herb end class WhitespaceNode < Node - attr_reader value: Herb::Token + attr_reader value: Herb::Token? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?) -> void # : () -> serialized_whitespace_node def to_hash: () -> serialized_whitespace_node + # : (Hash[untyped, untyped]) -> WhitespaceNode + def self.from_hash: (Hash[untyped, untyped]) -> WhitespaceNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -401,11 +449,11 @@ module Herb end class ERBContentNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader content: Herb::Token + attr_reader content: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? attr_reader analyzed_ruby: nil @@ -413,12 +461,15 @@ module Herb attr_reader valid: bool - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, nil, bool, bool) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, nil, bool, bool) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, nil, bool, bool) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, nil, bool, bool) -> void # : () -> serialized_erb_content_node def to_hash: () -> serialized_erb_content_node + # : (Hash[untyped, untyped]) -> ERBContentNode + def self.from_hash: (Hash[untyped, untyped]) -> ERBContentNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -436,18 +487,21 @@ module Herb end class ERBEndNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader content: Herb::Token + attr_reader content: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?) -> void # : () -> serialized_erb_end_node def to_hash: () -> serialized_erb_end_node + # : (Hash[untyped, untyped]) -> ERBEndNode + def self.from_hash: (Hash[untyped, untyped]) -> ERBEndNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -465,20 +519,23 @@ module Herb end class ERBElseNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader content: Herb::Token + attr_reader content: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? attr_reader statements: Array[Herb::AST::Node] - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node]) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node]) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node]) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node]) -> void # : () -> serialized_erb_else_node def to_hash: () -> serialized_erb_else_node + # : (Hash[untyped, untyped]) -> ERBElseNode + def self.from_hash: (Hash[untyped, untyped]) -> ERBElseNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -496,24 +553,27 @@ module Herb end class ERBIfNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader content: Herb::Token + attr_reader content: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? attr_reader statements: Array[Herb::AST::Node] - attr_reader subsequent: Herb::AST::Node + attr_reader subsequent: Herb::AST::Node? - attr_reader end_node: Herb::AST::ERBEndNode + attr_reader end_node: Herb::AST::ERBEndNode? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::Node, Herb::AST::ERBEndNode) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::Node, Herb::AST::ERBEndNode) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::Node?, Herb::AST::ERBEndNode?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::Node?, Herb::AST::ERBEndNode?) -> void # : () -> serialized_erb_if_node def to_hash: () -> serialized_erb_if_node + # : (Hash[untyped, untyped]) -> ERBIfNode + def self.from_hash: (Hash[untyped, untyped]) -> ERBIfNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -531,22 +591,25 @@ module Herb end class ERBBlockNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader content: Herb::Token + attr_reader content: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? attr_reader body: Array[Herb::AST::Node] - attr_reader end_node: Herb::AST::ERBEndNode + attr_reader end_node: Herb::AST::ERBEndNode? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBEndNode) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBEndNode) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBEndNode?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBEndNode?) -> void # : () -> serialized_erb_block_node def to_hash: () -> serialized_erb_block_node + # : (Hash[untyped, untyped]) -> ERBBlockNode + def self.from_hash: (Hash[untyped, untyped]) -> ERBBlockNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -564,20 +627,23 @@ module Herb end class ERBWhenNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader content: Herb::Token + attr_reader content: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? attr_reader statements: Array[Herb::AST::Node] - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node]) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node]) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node]) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node]) -> void # : () -> serialized_erb_when_node def to_hash: () -> serialized_erb_when_node + # : (Hash[untyped, untyped]) -> ERBWhenNode + def self.from_hash: (Hash[untyped, untyped]) -> ERBWhenNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -595,26 +661,29 @@ module Herb end class ERBCaseNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader content: Herb::Token + attr_reader content: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? attr_reader children: Array[Herb::AST::Node] attr_reader conditions: Array[Herb::AST::ERBWhenNode] - attr_reader else_clause: Herb::AST::ERBElseNode + attr_reader else_clause: Herb::AST::ERBElseNode? - attr_reader end_node: Herb::AST::ERBEndNode + attr_reader end_node: Herb::AST::ERBEndNode? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Array[Herb::AST::ERBWhenNode], Herb::AST::ERBElseNode, Herb::AST::ERBEndNode) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Array[Herb::AST::ERBWhenNode], Herb::AST::ERBElseNode, Herb::AST::ERBEndNode) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Array[Herb::AST::ERBWhenNode], Herb::AST::ERBElseNode?, Herb::AST::ERBEndNode?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Array[Herb::AST::ERBWhenNode], Herb::AST::ERBElseNode?, Herb::AST::ERBEndNode?) -> void # : () -> serialized_erb_case_node def to_hash: () -> serialized_erb_case_node + # : (Hash[untyped, untyped]) -> ERBCaseNode + def self.from_hash: (Hash[untyped, untyped]) -> ERBCaseNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -632,26 +701,29 @@ module Herb end class ERBCaseMatchNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader content: Herb::Token + attr_reader content: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? attr_reader children: Array[Herb::AST::Node] attr_reader conditions: Array[Herb::AST::ERBInNode] - attr_reader else_clause: Herb::AST::ERBElseNode + attr_reader else_clause: Herb::AST::ERBElseNode? - attr_reader end_node: Herb::AST::ERBEndNode + attr_reader end_node: Herb::AST::ERBEndNode? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Array[Herb::AST::ERBInNode], Herb::AST::ERBElseNode, Herb::AST::ERBEndNode) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Array[Herb::AST::ERBInNode], Herb::AST::ERBElseNode, Herb::AST::ERBEndNode) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Array[Herb::AST::ERBInNode], Herb::AST::ERBElseNode?, Herb::AST::ERBEndNode?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Array[Herb::AST::ERBInNode], Herb::AST::ERBElseNode?, Herb::AST::ERBEndNode?) -> void # : () -> serialized_erb_case_match_node def to_hash: () -> serialized_erb_case_match_node + # : (Hash[untyped, untyped]) -> ERBCaseMatchNode + def self.from_hash: (Hash[untyped, untyped]) -> ERBCaseMatchNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -669,22 +741,25 @@ module Herb end class ERBWhileNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader content: Herb::Token + attr_reader content: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? attr_reader statements: Array[Herb::AST::Node] - attr_reader end_node: Herb::AST::ERBEndNode + attr_reader end_node: Herb::AST::ERBEndNode? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBEndNode) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBEndNode) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBEndNode?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBEndNode?) -> void # : () -> serialized_erb_while_node def to_hash: () -> serialized_erb_while_node + # : (Hash[untyped, untyped]) -> ERBWhileNode + def self.from_hash: (Hash[untyped, untyped]) -> ERBWhileNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -702,22 +777,25 @@ module Herb end class ERBUntilNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader content: Herb::Token + attr_reader content: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? attr_reader statements: Array[Herb::AST::Node] - attr_reader end_node: Herb::AST::ERBEndNode + attr_reader end_node: Herb::AST::ERBEndNode? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBEndNode) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBEndNode) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBEndNode?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBEndNode?) -> void # : () -> serialized_erb_until_node def to_hash: () -> serialized_erb_until_node + # : (Hash[untyped, untyped]) -> ERBUntilNode + def self.from_hash: (Hash[untyped, untyped]) -> ERBUntilNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -735,22 +813,25 @@ module Herb end class ERBForNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader content: Herb::Token + attr_reader content: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? attr_reader statements: Array[Herb::AST::Node] - attr_reader end_node: Herb::AST::ERBEndNode + attr_reader end_node: Herb::AST::ERBEndNode? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBEndNode) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBEndNode) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBEndNode?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBEndNode?) -> void # : () -> serialized_erb_for_node def to_hash: () -> serialized_erb_for_node + # : (Hash[untyped, untyped]) -> ERBForNode + def self.from_hash: (Hash[untyped, untyped]) -> ERBForNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -768,22 +849,25 @@ module Herb end class ERBRescueNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader content: Herb::Token + attr_reader content: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? attr_reader statements: Array[Herb::AST::Node] - attr_reader subsequent: Herb::AST::ERBRescueNode + attr_reader subsequent: Herb::AST::ERBRescueNode? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBRescueNode) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBRescueNode) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBRescueNode?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBRescueNode?) -> void # : () -> serialized_erb_rescue_node def to_hash: () -> serialized_erb_rescue_node + # : (Hash[untyped, untyped]) -> ERBRescueNode + def self.from_hash: (Hash[untyped, untyped]) -> ERBRescueNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -801,20 +885,23 @@ module Herb end class ERBEnsureNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader content: Herb::Token + attr_reader content: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? attr_reader statements: Array[Herb::AST::Node] - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node]) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node]) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node]) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node]) -> void # : () -> serialized_erb_ensure_node def to_hash: () -> serialized_erb_ensure_node + # : (Hash[untyped, untyped]) -> ERBEnsureNode + def self.from_hash: (Hash[untyped, untyped]) -> ERBEnsureNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -832,28 +919,31 @@ module Herb end class ERBBeginNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader content: Herb::Token + attr_reader content: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? attr_reader statements: Array[Herb::AST::Node] - attr_reader rescue_clause: Herb::AST::ERBRescueNode + attr_reader rescue_clause: Herb::AST::ERBRescueNode? - attr_reader else_clause: Herb::AST::ERBElseNode + attr_reader else_clause: Herb::AST::ERBElseNode? - attr_reader ensure_clause: Herb::AST::ERBEnsureNode + attr_reader ensure_clause: Herb::AST::ERBEnsureNode? - attr_reader end_node: Herb::AST::ERBEndNode + attr_reader end_node: Herb::AST::ERBEndNode? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBRescueNode, Herb::AST::ERBElseNode, Herb::AST::ERBEnsureNode, Herb::AST::ERBEndNode) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBRescueNode, Herb::AST::ERBElseNode, Herb::AST::ERBEnsureNode, Herb::AST::ERBEndNode) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBRescueNode?, Herb::AST::ERBElseNode?, Herb::AST::ERBEnsureNode?, Herb::AST::ERBEndNode?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBRescueNode?, Herb::AST::ERBElseNode?, Herb::AST::ERBEnsureNode?, Herb::AST::ERBEndNode?) -> void # : () -> serialized_erb_begin_node def to_hash: () -> serialized_erb_begin_node + # : (Hash[untyped, untyped]) -> ERBBeginNode + def self.from_hash: (Hash[untyped, untyped]) -> ERBBeginNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -871,24 +961,27 @@ module Herb end class ERBUnlessNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader content: Herb::Token + attr_reader content: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? attr_reader statements: Array[Herb::AST::Node] - attr_reader else_clause: Herb::AST::ERBElseNode + attr_reader else_clause: Herb::AST::ERBElseNode? - attr_reader end_node: Herb::AST::ERBEndNode + attr_reader end_node: Herb::AST::ERBEndNode? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBElseNode, Herb::AST::ERBEndNode) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node], Herb::AST::ERBElseNode, Herb::AST::ERBEndNode) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBElseNode?, Herb::AST::ERBEndNode?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node], Herb::AST::ERBElseNode?, Herb::AST::ERBEndNode?) -> void # : () -> serialized_erb_unless_node def to_hash: () -> serialized_erb_unless_node + # : (Hash[untyped, untyped]) -> ERBUnlessNode + def self.from_hash: (Hash[untyped, untyped]) -> ERBUnlessNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -906,18 +999,21 @@ module Herb end class ERBYieldNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader content: Herb::Token + attr_reader content: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?) -> void # : () -> serialized_erb_yield_node def to_hash: () -> serialized_erb_yield_node + # : (Hash[untyped, untyped]) -> ERBYieldNode + def self.from_hash: (Hash[untyped, untyped]) -> ERBYieldNode + # : (Visitor) -> void def accept: (Visitor) -> void @@ -935,20 +1031,23 @@ module Herb end class ERBInNode < Node - attr_reader tag_opening: Herb::Token + attr_reader tag_opening: Herb::Token? - attr_reader content: Herb::Token + attr_reader content: Herb::Token? - attr_reader tag_closing: Herb::Token + attr_reader tag_closing: Herb::Token? attr_reader statements: Array[Herb::AST::Node] - # : (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node]) -> void - def initialize: (String, Location, Array[Herb::Errors::Error], Herb::Token, Herb::Token, Herb::Token, Array[Herb::AST::Node]) -> void + # : (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node]) -> void + def initialize: (String, Location?, Array[Herb::Errors::Error], Herb::Token?, Herb::Token?, Herb::Token?, Array[Herb::AST::Node]) -> void # : () -> serialized_erb_in_node def to_hash: () -> serialized_erb_in_node + # : (Hash[untyped, untyped]) -> ERBInNode + def self.from_hash: (Hash[untyped, untyped]) -> ERBInNode + # : (Visitor) -> void def accept: (Visitor) -> void diff --git a/sig/herb/backend.rbs b/sig/herb/backend.rbs new file mode 100644 index 000000000..9f3a6d9ca --- /dev/null +++ b/sig/herb/backend.rbs @@ -0,0 +1,69 @@ +# Generated from lib/herb/backend.rb with RBS::Inline + +module Herb + class Backend + attr_reader loaded: untyped + + def initialize: () -> untyped + + def load: () -> untyped + + def loaded?: () -> untyped + + def lex: (untyped source) -> untyped + + def lex_file: (untyped path) -> untyped + + def parse: (untyped source, ?untyped options) -> untyped + + def parse_file: (untyped path, ?untyped options) -> untyped + + def extract_ruby: (untyped source) -> untyped + + def extract_html: (untyped source) -> untyped + + def version: () -> untyped + + def format: (untyped source, ?untyped options) -> untyped + + def format_file: (untyped path, ?untyped options) -> untyped + + def lint: (untyped source, ?untyped options) -> untyped + + def lint_file: (untyped path, ?untyped options) -> untyped + + def print_node: (untyped node, ?untyped options) -> untyped + + def backend_name: () -> untyped + + def perform_load: () -> untyped + + def perform_lex: (untyped source) -> untyped + + def perform_lex_file: (untyped path) -> untyped + + def perform_parse: (untyped source, untyped options) -> untyped + + def perform_parse_file: (untyped path, untyped options) -> untyped + + def perform_extract_ruby: (untyped source) -> untyped + + def perform_extract_html: (untyped source) -> untyped + + def backend_version: () -> untyped + + def perform_format: (untyped source, untyped options) -> untyped + + def perform_format_file: (untyped path, untyped options) -> untyped + + def perform_lint: (untyped source, untyped options) -> untyped + + def perform_lint_file: (untyped path, untyped options) -> untyped + + def perform_print_node: (untyped node, untyped options) -> untyped + + private + + def ensure_loaded!: () -> untyped + end +end diff --git a/sig/herb/backend_loader.rbs b/sig/herb/backend_loader.rbs new file mode 100644 index 000000000..1eb9cd549 --- /dev/null +++ b/sig/herb/backend_loader.rbs @@ -0,0 +1,21 @@ +# Generated from lib/herb/backend_loader.rb with RBS::Inline + +module Herb + class BackendLoader + BACKENDS: untyped + + DEFAULT_PRIORITY: untyped + + def self.load: (?untyped backend_name) -> untyped + + def self.detect_backend: () -> untyped + + def self.available_backends: () -> untyped + + private def self.load_native_backend: () -> untyped + + private def self.load_node_backend: () -> untyped + + private def self.test_backend: (untyped backend_name) -> untyped + end +end diff --git a/sig/herb/backends/ffi_backend.rbs b/sig/herb/backends/ffi_backend.rbs new file mode 100644 index 000000000..8a0f08262 --- /dev/null +++ b/sig/herb/backends/ffi_backend.rbs @@ -0,0 +1,29 @@ +# Generated from lib/herb/backends/ffi_backend.rb with RBS::Inline + +module Herb + module Backends + class FFIBackend < Backend + def perform_load: () -> untyped + + def perform_lex: (untyped source) -> untyped + + def perform_lex_file: (untyped path) -> untyped + + def perform_parse: (untyped source, untyped options) -> untyped + + def perform_parse_file: (untyped path, untyped options) -> untyped + + def perform_extract_ruby: (untyped source) -> untyped + + def perform_extract_html: (untyped source) -> untyped + + def backend_version: () -> untyped + + def perform_format: (untyped source, untyped options) -> untyped + + def perform_lint: (untyped source, untyped options) -> untyped + + def perform_print_node: (untyped node, untyped options) -> untyped + end + end +end diff --git a/sig/herb/backends/native_backend.rbs b/sig/herb/backends/native_backend.rbs new file mode 100644 index 000000000..b54c687d6 --- /dev/null +++ b/sig/herb/backends/native_backend.rbs @@ -0,0 +1,29 @@ +# Generated from lib/herb/backends/native_backend.rb with RBS::Inline + +module Herb + module Backends + class NativeBackend < Backend + def perform_load: () -> untyped + + def perform_lex: (untyped source) -> untyped + + def perform_lex_file: (untyped path) -> untyped + + def perform_parse: (untyped source, untyped options) -> untyped + + def perform_parse_file: (untyped path, untyped options) -> untyped + + def perform_extract_ruby: (untyped source) -> untyped + + def perform_extract_html: (untyped source) -> untyped + + def backend_version: () -> untyped + + def perform_format: (untyped source, untyped options) -> untyped + + def perform_lint: (untyped source, untyped options) -> untyped + + def perform_print_node: (untyped node, untyped options) -> untyped + end + end +end diff --git a/sig/herb/backends/node_backend.rbs b/sig/herb/backends/node_backend.rbs new file mode 100644 index 000000000..6d7d4311e --- /dev/null +++ b/sig/herb/backends/node_backend.rbs @@ -0,0 +1,49 @@ +# Generated from lib/herb/backends/node_backend.rb with RBS::Inline + +module Herb + module Backends + class NodeBackend < Backend + attr_reader nodo: untyped + + attr_reader herb_node: untyped + + def perform_load: () -> untyped + + def perform_lex: (untyped source) -> untyped + + def perform_lex_file: (untyped path) -> untyped + + def perform_parse: (untyped source, untyped options) -> untyped + + def perform_parse_file: (untyped path, untyped options) -> untyped + + def perform_extract_ruby: (untyped source) -> untyped + + def perform_extract_html: (untyped source) -> untyped + + def perform_format: (untyped source, untyped options) -> untyped + + def perform_lint: (untyped source, untyped options) -> untyped + + def perform_print_node: (untyped node, untyped options) -> untyped + + def backend_version: () -> untyped + + private + + def setup_node_environment: () -> untyped + + def find_node_binary: () -> untyped + + def find_node_modules_path: () -> untyped + + def convert_options: (untyped options) -> untyped + + def parse_lex_result: (untyped json_string) -> untyped + + def parse_parse_result: (untyped json_string) -> untyped + + def parse_lint_result: (untyped json_string) -> untyped + end + end +end diff --git a/sig/herb/backends/wasm_backend.rbs b/sig/herb/backends/wasm_backend.rbs new file mode 100644 index 000000000..edfcc0baa --- /dev/null +++ b/sig/herb/backends/wasm_backend.rbs @@ -0,0 +1,29 @@ +# Generated from lib/herb/backends/wasm_backend.rb with RBS::Inline + +module Herb + module Backends + class WASMBackend < Backend + def perform_load: () -> untyped + + def perform_lex: (untyped source) -> untyped + + def perform_lex_file: (untyped path) -> untyped + + def perform_parse: (untyped source, untyped options) -> untyped + + def perform_parse_file: (untyped path, untyped options) -> untyped + + def perform_extract_ruby: (untyped source) -> untyped + + def perform_extract_html: (untyped source) -> untyped + + def backend_version: () -> untyped + + def perform_format: (untyped source, untyped options) -> untyped + + def perform_lint: (untyped source, untyped options) -> untyped + + def perform_print_node: (untyped node, untyped options) -> untyped + end + end +end diff --git a/sig/herb/diagnostic.rbs b/sig/herb/diagnostic.rbs new file mode 100644 index 000000000..61b1f647b --- /dev/null +++ b/sig/herb/diagnostic.rbs @@ -0,0 +1,46 @@ +# Generated from lib/herb/diagnostic.rb with RBS::Inline + +module Herb + DIAGNOSTIC_SEVERITIES: Array[String] + + class Diagnostic + attr_reader message: String + + attr_reader location: Location? + + attr_reader severity: String + + attr_reader code: String? + + attr_reader source: String? + + # : (message: String, location: Location?, severity: String, ?code: String?, ?source: String?) -> void + def initialize: (message: String, location: Location?, severity: String, ?code: String?, ?source: String?) -> void + + # : () -> Hash[Symbol, untyped] + def to_h: () -> Hash[Symbol, untyped] + + # : (*untyped) -> String + def to_json: (*untyped) -> String + + # : () -> String + def to_s: () -> String + + # : () -> bool + def error?: () -> bool + + # : () -> bool + def warning?: () -> bool + + # : () -> bool + def info?: () -> bool + + # : () -> bool + def hint?: () -> bool + + private + + # : (String) -> String + def validate_severity: (String) -> String + end +end diff --git a/sig/herb/errors.rbs b/sig/herb/errors.rbs index ec1b47a57..f3aa80bc5 100644 --- a/sig/herb/errors.rbs +++ b/sig/herb/errors.rbs @@ -5,16 +5,19 @@ module Herb class Error attr_reader type: String - attr_reader location: Location + attr_reader location: Location? attr_reader message: String - # : (String, Location, String) -> void - def initialize: (String, Location, String) -> void + # : (String, Location?, String) -> void + def initialize: (String, Location?, String) -> void # : () -> serialized_error def to_hash: () -> serialized_error + # : (Hash[untyped, untyped]) -> Error + def self.from_hash: (Hash[untyped, untyped]) -> Error + # : () -> String def class_name: () -> String @@ -35,8 +38,8 @@ module Herb attr_reader found: String - # : (String, Location, String, String, String, String) -> void - def initialize: (String, Location, String, String, String, String) -> void + # : (String, Location?, String, String, String, String) -> void + def initialize: (String, Location?, String, String, String, String) -> void # : () -> String def inspect: () -> String @@ -44,6 +47,9 @@ module Herb # : () -> serialized_unexpected_error def to_hash: () -> serialized_unexpected_error + # : (Hash[untyped, untyped]) -> UnexpectedError + def self.from_hash: (Hash[untyped, untyped]) -> UnexpectedError + # : (?Integer) -> String def tree_inspect: (?Integer) -> String end @@ -51,10 +57,10 @@ module Herb class UnexpectedTokenError < Error attr_reader expected_type: String - attr_reader found: Herb::Token + attr_reader found: Herb::Token? - # : (String, Location, String, String, Herb::Token) -> void - def initialize: (String, Location, String, String, Herb::Token) -> void + # : (String, Location?, String, String, Herb::Token?) -> void + def initialize: (String, Location?, String, String, Herb::Token?) -> void # : () -> String def inspect: () -> String @@ -62,15 +68,18 @@ module Herb # : () -> serialized_unexpected_token_error def to_hash: () -> serialized_unexpected_token_error + # : (Hash[untyped, untyped]) -> UnexpectedTokenError + def self.from_hash: (Hash[untyped, untyped]) -> UnexpectedTokenError + # : (?Integer) -> String def tree_inspect: (?Integer) -> String end class MissingOpeningTagError < Error - attr_reader closing_tag: Herb::Token + attr_reader closing_tag: Herb::Token? - # : (String, Location, String, Herb::Token) -> void - def initialize: (String, Location, String, Herb::Token) -> void + # : (String, Location?, String, Herb::Token?) -> void + def initialize: (String, Location?, String, Herb::Token?) -> void # : () -> String def inspect: () -> String @@ -78,15 +87,18 @@ module Herb # : () -> serialized_missing_opening_tag_error def to_hash: () -> serialized_missing_opening_tag_error + # : (Hash[untyped, untyped]) -> MissingOpeningTagError + def self.from_hash: (Hash[untyped, untyped]) -> MissingOpeningTagError + # : (?Integer) -> String def tree_inspect: (?Integer) -> String end class MissingClosingTagError < Error - attr_reader opening_tag: Herb::Token + attr_reader opening_tag: Herb::Token? - # : (String, Location, String, Herb::Token) -> void - def initialize: (String, Location, String, Herb::Token) -> void + # : (String, Location?, String, Herb::Token?) -> void + def initialize: (String, Location?, String, Herb::Token?) -> void # : () -> String def inspect: () -> String @@ -94,17 +106,20 @@ module Herb # : () -> serialized_missing_closing_tag_error def to_hash: () -> serialized_missing_closing_tag_error + # : (Hash[untyped, untyped]) -> MissingClosingTagError + def self.from_hash: (Hash[untyped, untyped]) -> MissingClosingTagError + # : (?Integer) -> String def tree_inspect: (?Integer) -> String end class TagNamesMismatchError < Error - attr_reader opening_tag: Herb::Token + attr_reader opening_tag: Herb::Token? - attr_reader closing_tag: Herb::Token + attr_reader closing_tag: Herb::Token? - # : (String, Location, String, Herb::Token, Herb::Token) -> void - def initialize: (String, Location, String, Herb::Token, Herb::Token) -> void + # : (String, Location?, String, Herb::Token?, Herb::Token?) -> void + def initialize: (String, Location?, String, Herb::Token?, Herb::Token?) -> void # : () -> String def inspect: () -> String @@ -112,17 +127,20 @@ module Herb # : () -> serialized_tag_names_mismatch_error def to_hash: () -> serialized_tag_names_mismatch_error + # : (Hash[untyped, untyped]) -> TagNamesMismatchError + def self.from_hash: (Hash[untyped, untyped]) -> TagNamesMismatchError + # : (?Integer) -> String def tree_inspect: (?Integer) -> String end class QuotesMismatchError < Error - attr_reader opening_quote: Herb::Token + attr_reader opening_quote: Herb::Token? - attr_reader closing_quote: Herb::Token + attr_reader closing_quote: Herb::Token? - # : (String, Location, String, Herb::Token, Herb::Token) -> void - def initialize: (String, Location, String, Herb::Token, Herb::Token) -> void + # : (String, Location?, String, Herb::Token?, Herb::Token?) -> void + def initialize: (String, Location?, String, Herb::Token?, Herb::Token?) -> void # : () -> String def inspect: () -> String @@ -130,19 +148,22 @@ module Herb # : () -> serialized_quotes_mismatch_error def to_hash: () -> serialized_quotes_mismatch_error + # : (Hash[untyped, untyped]) -> QuotesMismatchError + def self.from_hash: (Hash[untyped, untyped]) -> QuotesMismatchError + # : (?Integer) -> String def tree_inspect: (?Integer) -> String end class VoidElementClosingTagError < Error - attr_reader tag_name: Herb::Token + attr_reader tag_name: Herb::Token? attr_reader expected: String attr_reader found: String - # : (String, Location, String, Herb::Token, String, String) -> void - def initialize: (String, Location, String, Herb::Token, String, String) -> void + # : (String, Location?, String, Herb::Token?, String, String) -> void + def initialize: (String, Location?, String, Herb::Token?, String, String) -> void # : () -> String def inspect: () -> String @@ -150,15 +171,18 @@ module Herb # : () -> serialized_void_element_closing_tag_error def to_hash: () -> serialized_void_element_closing_tag_error + # : (Hash[untyped, untyped]) -> VoidElementClosingTagError + def self.from_hash: (Hash[untyped, untyped]) -> VoidElementClosingTagError + # : (?Integer) -> String def tree_inspect: (?Integer) -> String end class UnclosedElementError < Error - attr_reader opening_tag: Herb::Token + attr_reader opening_tag: Herb::Token? - # : (String, Location, String, Herb::Token) -> void - def initialize: (String, Location, String, Herb::Token) -> void + # : (String, Location?, String, Herb::Token?) -> void + def initialize: (String, Location?, String, Herb::Token?) -> void # : () -> String def inspect: () -> String @@ -166,6 +190,9 @@ module Herb # : () -> serialized_unclosed_element_error def to_hash: () -> serialized_unclosed_element_error + # : (Hash[untyped, untyped]) -> UnclosedElementError + def self.from_hash: (Hash[untyped, untyped]) -> UnclosedElementError + # : (?Integer) -> String def tree_inspect: (?Integer) -> String end @@ -177,8 +204,8 @@ module Herb attr_reader level: String - # : (String, Location, String, String, String, String) -> void - def initialize: (String, Location, String, String, String, String) -> void + # : (String, Location?, String, String, String, String) -> void + def initialize: (String, Location?, String, String, String, String) -> void # : () -> String def inspect: () -> String @@ -186,6 +213,9 @@ module Herb # : () -> serialized_ruby_parse_error def to_hash: () -> serialized_ruby_parse_error + # : (Hash[untyped, untyped]) -> RubyParseError + def self.from_hash: (Hash[untyped, untyped]) -> RubyParseError + # : (?Integer) -> String def tree_inspect: (?Integer) -> String end diff --git a/sig/herb/lex_result.rbs b/sig/herb/lex_result.rbs index cfb7db0ed..a0895e396 100644 --- a/sig/herb/lex_result.rbs +++ b/sig/herb/lex_result.rbs @@ -12,5 +12,8 @@ module Herb # : () -> bool def failed?: () -> bool + + # : (Hash[untyped, untyped]) -> LexResult + def self.from_hash: (Hash[untyped, untyped]) -> LexResult end end diff --git a/sig/herb/lint_offense.rbs b/sig/herb/lint_offense.rbs new file mode 100644 index 000000000..71e2a8907 --- /dev/null +++ b/sig/herb/lint_offense.rbs @@ -0,0 +1,19 @@ +# Generated from lib/herb/lint_offense.rb with RBS::Inline + +module Herb + class LintOffense < Diagnostic + attr_reader rule: String + + # : (message: String, location: Location?, severity: String, rule: String, ?code: String?, ?source: String?) -> void + def initialize: (message: String, location: Location?, severity: String, rule: String, ?code: String?, ?source: String?) -> void + + # : () -> Hash[Symbol, untyped] + def to_h: () -> Hash[Symbol, untyped] + + # : () -> String + def to_s: () -> String + + # : (Hash[untyped, untyped]) -> LintOffense + def self.from_hash: (Hash[untyped, untyped]) -> LintOffense + end +end diff --git a/sig/herb/lint_result.rbs b/sig/herb/lint_result.rbs new file mode 100644 index 000000000..e5f914fb5 --- /dev/null +++ b/sig/herb/lint_result.rbs @@ -0,0 +1,61 @@ +# Generated from lib/herb/lint_result.rb with RBS::Inline + +module Herb + class LintResult + attr_reader offenses: Array[LintOffense] + + # : (?Array[LintOffense | Hash[untyped, untyped]]) -> void + def initialize: (?Array[LintOffense | Hash[untyped, untyped]]) -> void + + # : () -> Integer + def errors: () -> Integer + + # : () -> Integer + def warnings: () -> Integer + + # : () -> Integer + def infos: () -> Integer + + # : () -> Integer + def hints: () -> Integer + + # : () -> Integer + def total_offenses: () -> Integer + + # : () -> bool + def success?: () -> bool + + # : () -> bool + def clean?: () -> bool + + # : (String) -> Array[LintOffense] + def offenses_by_severity: (String) -> Array[LintOffense] + + # : () -> Array[LintOffense] + def error_offenses: () -> Array[LintOffense] + + # : () -> Array[LintOffense] + def warning_offenses: () -> Array[LintOffense] + + # : () -> Array[LintOffense] + def info_offenses: () -> Array[LintOffense] + + # : () -> Array[LintOffense] + def hint_offenses: () -> Array[LintOffense] + + # : (String) -> Array[LintOffense] + def offenses_for_rule: (String) -> Array[LintOffense] + + # : () -> Hash[Symbol, untyped] + def to_h: () -> Hash[Symbol, untyped] + + # : (?untyped) -> String + def to_json: (?untyped) -> String + + # : () -> String + def to_s: () -> String + + # : (Hash[untyped, untyped]) -> LintResult + def self.from_hash: (Hash[untyped, untyped]) -> LintResult + end +end diff --git a/sig/herb/location.rbs b/sig/herb/location.rbs index b10799f1d..97c85b376 100644 --- a/sig/herb/location.rbs +++ b/sig/herb/location.rbs @@ -15,9 +15,15 @@ module Herb # : (Integer, Integer, Integer, Integer) -> Location def self.[]: (Integer, Integer, Integer, Integer) -> Location + # : (Hash[untyped, untyped]|nil) -> Location? + def self.from_hash: (Hash[untyped, untyped] | nil) -> Location? + # : () -> serialized_location def to_hash: () -> serialized_location + # : () -> serialized_location + def to_h: () -> serialized_location + # : (?untyped) -> String def to_json: (?untyped) -> String diff --git a/sig/herb/parse_result.rbs b/sig/herb/parse_result.rbs index 6e0801f38..bbfbfb82b 100644 --- a/sig/herb/parse_result.rbs +++ b/sig/herb/parse_result.rbs @@ -19,7 +19,13 @@ module Herb # : () -> String def pretty_errors: () -> String + # : () -> String + def to_source: () -> String + # : (Visitor) -> void def visit: (Visitor) -> void + + # : (Hash[untyped, untyped]) -> ParseResult + def self.from_hash: (Hash[untyped, untyped]) -> ParseResult end end diff --git a/sig/herb/position.rbs b/sig/herb/position.rbs index cb0c33ee4..5583c72dc 100644 --- a/sig/herb/position.rbs +++ b/sig/herb/position.rbs @@ -15,9 +15,15 @@ module Herb # : (Integer, Integer) -> Position def self.from: (Integer, Integer) -> Position + # : (Hash[untyped, untyped]|nil) -> Position? + def self.from_hash: (Hash[untyped, untyped] | nil) -> Position? + # : () -> serialized_position def to_hash: () -> serialized_position + # : () -> serialized_position + def to_h: () -> serialized_position + # : (?untyped) -> String def to_json: (?untyped) -> String diff --git a/sig/herb/range.rbs b/sig/herb/range.rbs index 9b6d02947..a85bec433 100644 --- a/sig/herb/range.rbs +++ b/sig/herb/range.rbs @@ -15,6 +15,9 @@ module Herb # : (Integer, Integer) -> Range def self.from: (Integer, Integer) -> Range + # : (Array[untyped]|Hash[untyped, untyped]|nil) -> Range? + def self.from_hash: (Array[untyped] | Hash[untyped, untyped] | nil) -> Range? + # : () -> serialized_range def to_a: () -> serialized_range diff --git a/sig/herb/token.rbs b/sig/herb/token.rbs index d29c75c06..6ef729567 100644 --- a/sig/herb/token.rbs +++ b/sig/herb/token.rbs @@ -16,6 +16,9 @@ module Herb # : () -> serialized_token def to_hash: () -> serialized_token + # : (Hash[untyped, untyped]?) -> Token? + def self.from_hash: (Hash[untyped, untyped]?) -> Token? + # : (?untyped) -> String def to_json: (?untyped) -> String diff --git a/sig/herb/warnings.rbs b/sig/herb/warnings.rbs index 4da04d0eb..8096091b7 100644 --- a/sig/herb/warnings.rbs +++ b/sig/herb/warnings.rbs @@ -5,16 +5,19 @@ module Herb class Warning attr_reader type: String - attr_reader location: Location + attr_reader location: Location? attr_reader message: String - # : (String, Location, String) -> void - def initialize: (String, Location, String) -> void + # : (String, Location?, String) -> void + def initialize: (String, Location?, String) -> void # : () -> serialized_warning def to_hash: () -> serialized_warning + # : (Hash[untyped, untyped]) -> Warning + def self.from_hash: (Hash[untyped, untyped]) -> Warning + # : () -> String def class_name: () -> String diff --git a/sig/native_backend.rbs b/sig/native_backend.rbs new file mode 100644 index 000000000..e2b71491b --- /dev/null +++ b/sig/native_backend.rbs @@ -0,0 +1,17 @@ +# For methods dynamically defined when requiring the Ruby C-Extension +# +# See: ext/herb/native_backend.c + +module Herb + module Backends + class NativeBackend < Backend + def c_perform_lex: (String) -> untyped + def c_perform_lex_file: (String) -> untyped + def c_perform_parse: (String, Hash[untyped, untyped]) -> untyped + def c_perform_parse_file: (String, Hash[untyped, untyped]) -> untyped + def c_perform_extract_ruby: (String) -> untyped + def c_perform_extract_html: (String) -> untyped + def c_backend_version: () -> String? + end + end +end diff --git a/stargazers-2025-08-23.txt b/stargazers-2025-08-23.txt new file mode 100644 index 000000000..f559f1094 --- /dev/null +++ b/stargazers-2025-08-23.txt @@ -0,0 +1,665 @@ +0exp +0rientd +0xD7ba952CE8A0976e8d9852b7649bf01c30146 +0xMostafa +29decibel +3colorr +a-chris +aaronvenezia +Abdulwahaab710 +acetinick +acidtib +AdamFreemer +admtnnr +adrianthedev +adrienpoly +aergonaut +afalkear +afmicc +afr114 +aglio +AGNutGH +agoos-nbcuni +AhmadMajid +ajsharp +aki77 +albertico +alec-c4 +alexgufler +alextrueman +alfakini +alfetahe +alfredoreduarte +alperenbozkurt +alphabetek +alxbnct +am1006 +amiralles +anderson +AndresGarciadelaHuerta +andrew +andrewhwaller +andrewmcodes +andrewnovykov +andy4thehuynh +andycandrea +andyhennie +anil-adepu +anonosuke +anthonyamar +appyspaces +aprotsyk +Apupsis +arturaspiksrys +as181920 +asenkovskiy +aspleenic +astrocket +atulvishw240 +auslach +axcochrane +ayb +b08x +badenkov +baku01 +basilmeer +baxter2 +bcgianni +bdavidxyz +bdougie +BedeDD +Belibaste +ben-greenwood +ben-gy +bendangelo +benguillet +benjbush +Benoit-Baumann +benwalks +Bergrebell +bibendi +BilalBudhani +billabel +billthompson +binilsn +bishalg +bitshaker +bjarkevad +bkenny +blasterpal +bluengreen +bluerssen +bmichelsen +bongole +bonty +bookingninja +bradgessler +bradpurchase +brandonzylstra +brendanwb10 +brentkearney +bricolage +brkn +brossi +brucruz +bryanbeshore +bryanmorrow +BuffaloDrew +bunnahabhain +candland +carlson +cdaviis +celina-lopez +chadwilken +champagnepappi +charlimmelman +CheckerFlow +Ches-ctrl +chokkoyamada +chops +chrisedington +chriskjaer +christiangenco +christopherstyles +chtzvt +chubchenko +chukitow +ciscoLegrand +codescaptain +colinux +crespire +cs3b +cuneyter +cyril +cyrilmayance +czhu12 +czj +damireh +dangerousbeans +Daniel-Brai +danielabar +danielpuglisi +danielwestendorf +danwetherald +dapeng2018 +davebream +davedkg +davidemerli +davidstump +davidteren +db0sch +dbates0623 +dchuk +deanpcmad +defeed +defifund +defkode +deivinsontejeda +denjamio +dennispaagman +dev-psi +Developerayo +dfioretti +diegofigueroa +dineyw23 +dkaplan88 +dlysenko +dmeremyanin +DmitrySychev +dongjinahn +douglara +doutatsu +dpaluy +DRBragg +dreamingblackcat +duckworth +dwaynemac +dyoshikawa +e12e +ebababi +edwinwills +egemen-dev +eiskrenkov +ekroon +elalemanyo +elarabyelaidy19 +eleazarer +emanuelhfarias +emaraschio +embs +emilford +emilianodellacasa +enprop +epugh +Eric-Guo +ericpowell03 +ericrochford +Erol +erozas +ersatzryan +ervinismu +estebarb +etewiah +Eth3rnit3 +Eunix +evdevdev +ewertoncodes +fagnerpereira +fdr +felipefontoura +ferrislucas +fetijashari +filipemendespi +finbarr +florianfynnweber +flyerhzm +fnmunhoz +frabr +fredoliveira +friendlyantz +Galathius +gamead +gando001 +garyhtou +geetfun +georgebaskervil +georgebellos +georgkreimer +gessivamjr +giedriusr +gilmda +gingermusketeer +gitpkj +gkosmo +glory-mfv +god4saken +goofansu +gregogalante +gregoryfm +grodowski +guilhermeteodoro +guizaols +GyozaGuy +hades0932 +hara-y-u +harshalbhakta +hayesr +headius +HenriqueRicardoFigueira +henrydjacob +heyitscoco +hiasinho +hiromitsusasaki +hkf +hoelmer +hokageCV +holamendi +hossamamer +howmuchcomputer +hsbt +huseyinbiyik +iamcoreyg +iaurg +iFloris +ignar +igorvazz +igrabes +igrigorik +ihoka +Ikass +ikraamg +ineedjet +intertwingle +iravikhatri +IrvanFza +ismaels +israeljrs +ivobenedito +ivy +j-manu +jake-duchesne +janko +janstol +jaredsmithse +jaryl +jason-riddle +jasonfantasia +jasperfurniss +JavaKoala +javid-gulamaliyev +jayelkaake +jaygooby +jbwl +jcsanti +jespr +jess +jgarber-cisco +jhash +Jholloway12 +jhuckabee +jimjimovich +jisuanjixue +jjstafford +jleo3 +jnettome +johannesschobel +johnlukeG +jon-sully +jonathanmdavies +jonbaer +JonCrawford +JordanForeman +JorgeDDW +josefarias +joshh45 +joshRpowell +joshshifman +joshuajansen +JoungSik +jpbalarini +jrramon +juandazapata +juliends +juliocabrera820 +juliusdelta +juniortada +justi +jylamont +kaspth +kbuckler +kcdragon +kdmgs110 +kemallette +kengreeff +KeyAmam +khalilgharbaoui +khasinski +kieranklaassen +kierr +kigster +kimihito +kirankarki +kjf +kjlape +kladaFOX +kmc79 +KodyKendall +koke2y +korakotlee +kristinadd +kristjanakkermann +krongk +krzkrzkrz +kurioscreative +kwent +landrywj +Largo +lazybios +lebedevilya +leemcalilly +leh +leonmezu1 +lfv89 +lHydra +LiamBateman +llenodo +localops-root +Lokideos +LordoftheManor7 +lorismaz +loumarven +luiscobot +lululau +lundie +mabras +mabutou +madddybit +magnum +MaherSaif +manuelmeurer +marclerodrigues +marcoroth +markets +MarkSurfas +masonhensley +master-of-null +masterT +mathieubrunpicard +mattparlane +MattRogish +mattwigham +mattzollinhofer +maxhungry +mcdonald-conor +mcusatech +mdomga +meg-gutshall +memon +meneerprins +Michael9311 +michaelroudnitski +michaelwapp +mikeastock +mikekosulin +mikeletscher +mikereczek +mikezaby +mikker +MiltonRen +MindRave +mintuhouse +MiroslavCsonka +mitchthebaker +mjberg +mnort9 +moneill +Morganjackson +MostafaAdly +mpressen +mqzhang +mtrefilek +musik +n-at-han-k +n00bvn +namiwang +namuit +natanaelmaia +nathanfishcircleci +nathanpalmer +navjeetc +nbluis +nemuba +neocoin +nertzy +netzfisch +neutrino +newdogcow +ngpestelos +nicieja +nickisnoble +nicogaldamez +nicolangr +niculistana +nik +niklasbabel +Nittarab +No-YE +nobodyiscertain +Novtopro +nrevko +Nuzair46 +nwarwick +ocarreterom +olimart +olinelson +omarluq +onk +Orphist +osjjames +OskarsEzerins +osmanok +otrub +oztofer +pablomarti +paderinandrey +panckreous +patvice +paulhenri-l +PauloPhagula +paultursuru +pfeiffer +phuwanart +pikitgb +pilgunboris +PiotrMocan +piscespieces +pkananen +plabaj +plattenschieber +playcase217 +postpostmodern +pranavbabu +preetpalS +princetechs +psylone +qen +qinmingyuan +raderj89 +raguila8 +railsdev +rajraj +ralder +rancar2 +randynov +rapcal +raphaelivan +ravinderrana +raysapida +relyks +renehernandez +renshuki +revans +rhiannon-io +ribeirojose +RichardBlair +ricsdeol +rince +riyaadh-abrahams +rockwellll +rogeriomarques +roma-glushko +RomanHood +ronb54 +Roriz +roshanabey2 +rpanachi +rslhdyt +rstrangh +ryanmaynard +rzkmr +S-H-GAMELINKS +safakferhatkaya +samuelcouch +samuelgiles +sanchal-coder +Sanchezdav +sarahg423 +sarbadaj +satotakumi +say2memohitverma +schappim +schmidie +Schwad +scicco +ScotterC +scottwhudson +seanhogge +SeanLF +SebastianSzturo +Sebhastien +SegundoRP +sergii +servatti +SerylLns +ShawnAukstak +shenaor +shkm +shouya31 +shreyas-makes +shunhikita +siegbenn +simon-marks-tcb +simonknoll +Sinhyeok +sirwolfgang +sisodiaa +skelz0r +Skullamortis +smathieu +spacechurro +spacepolice10 +SrAnthony +stancheta +stefanvermaas +steveclarke +stevegeek +StevenJL +stlucasgarcia +stream7 +strzibny +stuyam +su-sh +suaron +subelsky +sugi511 +suho +SujayPrabhu96 +sunenilausen +suras +swinner2 +tabishiqbal +talltorp +taltas +tamersalama +tanukiti1987 +tarakish +tarellel +taylorbryant +tehnuge +tgezginis +TheCovenant +thedarkside +thedayisntgray +thedumbtechguy +theill +therealadam +TheRealNeil +theSteveMitchell +thiagopecanha +thisIsLoading +thisugee +thomasgalibert +thomasklemm +thomaswitt +Thrry +tiknaosman +timsco +todddickerson +toddkummer +tomasmuller +TonsOfFun +tonymastrorio +topher6345 +tostca +tpaulshippy +trevorturk +tristanoneil +truongduyng +tvergara +ujackson +umairashas +umbrella-h +unabris +Uysim +varun2407 +versun +viamin +victor-github +viniciusmeneses +vishvish +visini +volpeo +vormwald +vsppedro +vsrnth +vtno +w3villa-deepanshu +WADI31dev +wagnerpereira +webmatze +websymphony +widget501 +williamherry +williamkennedy +wlaurance +wolfgang555 +wooyakob +wowinter13 +xkraty +xleotranx +xzgyb +yangtheman +yaroslav +yatish27 +yinho999 +yjacquin +ykpythemind +yogadevs +YuheiNakasaka +zackgilbert +zhephyn +znuamaan +zokioki +ZPVIP +zumkorn +zzJZzz diff --git a/templates/lib/herb/ast/nodes.rb.erb b/templates/lib/herb/ast/nodes.rb.erb index e51d71902..97ee648c8 100644 --- a/templates/lib/herb/ast/nodes.rb.erb +++ b/templates/lib/herb/ast/nodes.rb.erb @@ -1,12 +1,30 @@ module Herb module AST + # Dynamically reopen Node class to add node_from_hash method with knowledge of all node types + class Node + #: (Hash[untyped, untyped]?) -> Node? + def self.node_from_hash(data) + return nil unless data + + node_type = data[:type] || data["type"] + return nil unless node_type + + case node_type + <%- nodes.each do |node| -%> + when "<%= node.type %>" + <%= node.name %>.from_hash(data) + <%- end -%> + end + end + end + <%- nodes.each do |node| -%> class <%= node.name -%> < Node <%- node.fields.each do |field| -%> attr_reader :<%= field.name %> #: <%= field.ruby_type %> <%- end -%> - #: (<%= ["String", "Location", "Array[Herb::Errors::Error]", *node.fields.map(&:ruby_type)].join(", ") %>) -> void + #: (<%= ["String", "Location?", "Array[Herb::Errors::Error]", *node.fields.map(&:ruby_type)].join(", ") %>) -> void def initialize(<%= ["type", "location", "errors", *node.fields.map(&:name)].join(", ") %>) super(type, location, errors) <%- node.fields.each do |field| -%> @@ -22,11 +40,62 @@ module Herb def to_hash super.merge({ <%- node.fields.each do |field| -%> + <%- case field -%> + <%- when Herb::Template::NodeField -%> + <%= field.name %>: <%= field.name %>&.to_hash, + <%- when Herb::Template::ArrayField -%> + <%- if field.specific_kind&.end_with?("Node") -%> + <%= field.name %>: <%= field.name %>.map(&:to_hash), + <%- else -%> <%= field.name %>: <%= field.name %>, <%- end -%> + <%- when Herb::Template::TokenField -%> + <%= field.name %>: <%= field.name %>&.to_hash, + <%- else -%> + <%= field.name %>: <%= field.name %>, + <%- end -%> + <%- end -%> }) #: Herb::serialized_<%= node.human %> end + #: (Hash[untyped, untyped]) -> <%= node.name %> + def self.from_hash(data) + location = Location.from_hash(data[:location] || data["location"]) + errors = (data[:errors] || data["errors"] || []).map { |error| Herb::Errors::Error.from_hash(error) } + <%- node.fields.each do |field| -%> + <%- case field -%> + <%- when Herb::Template::StringField -%> + <%= field.name %> = data[:<%= field.name %>] || data["<%= field.name %>"] || "" + <%- when Herb::Template::TokenField -%> + <%= field.name %> = data[:<%= field.name %>] ? Herb::Token.from_hash(data[:<%= field.name %>] || data["<%= field.name %>"]) : nil + <%- when Herb::Template::BooleanField -%> + <%= field.name %> = if data.key?(:<%= field.name %>) + data[:<%= field.name %>] + else + data.key?("<%= field.name %>") ? data["<%= field.name %>"] : false + end + <%- when Herb::Template::NodeField -%> + <%- if field.specific_kind -%> + <%= field.name %>_node = data[:<%= field.name %>] ? Herb::AST::Node.node_from_hash(data[:<%= field.name %>] || data["<%= field.name %>"]) : nil + <%= field.name %> = <%= field.name %>_node #: Herb::AST::<%= field.specific_kind %>? + <%- else -%> + <%= field.name %> = data[:<%= field.name %>] ? Herb::AST::Node.node_from_hash(data[:<%= field.name %>] || data["<%= field.name %>"]) : nil + <%- end -%> + <%- when Herb::Template::ArrayField -%> + <%- if field.specific_kind&.end_with?("Node") -%> + <%= field.name %>_nodes = (data[:<%= field.name %>] || data["<%= field.name %>"] || []).map { |node_data| Herb::AST::Node.node_from_hash(node_data) }.compact + <%= field.name %> = <%= field.name %>_nodes #: Array[Herb::AST::<%= field.specific_kind %>] + <%- else -%> + <%= field.name %> = data[:<%= field.name %>] || data["<%= field.name %>"] || [] + <%- end -%> + <%- else -%> + <%= field.name %> = data[:<%= field.name %>] || data["<%= field.name %>"] + <%- end -%> + <%- end -%> + + new("<%= node.type %>", location, errors, <%= node.fields.map(&:name).join(", ") %>) + end + #: (Visitor) -> void def accept(visitor) visitor.visit_<%= node.human %>(self) @@ -60,7 +129,7 @@ module Herb output = +"" output += "@ #{node_name} " - output += location.tree_inspect + output += location&.tree_inspect || "(?)" output += "\n" output += inspect_errors(prefix: "<%= node.fields.any? ? "│ " : " " %>") diff --git a/templates/lib/herb/errors.rb.erb b/templates/lib/herb/errors.rb.erb index cc750f2fa..31ae943c9 100644 --- a/templates/lib/herb/errors.rb.erb +++ b/templates/lib/herb/errors.rb.erb @@ -1,4 +1,4 @@ -<%- base_arguments = [["type", "String"], ["location", "Location"], ["message", "String"]] -%> +<%- base_arguments = [["type", "String"], ["location", "Location?"], ["message", "String"]] -%> module Herb module Errors class Error @@ -22,6 +22,22 @@ module Herb } end + #: (Hash[untyped, untyped]) -> Error + def self.from_hash(data) + type = data[:type] || data["type"] || "" + location = Location.from_hash(data[:location] || data["location"]) + message = data[:message] || data["message"] || "" + + case type + <%- errors.each do |error| -%> + when "<%= error.type %>" + <%= error.name %>.from_hash(data) + <%- end -%> + else + new(type, location, message) + end + end + #: () -> String def class_name self.class.name || "Error" @@ -72,11 +88,30 @@ module Herb }) #: Herb::serialized_<%= error.human %> end + #: (Hash[untyped, untyped]) -> <%= error.name %> + def self.from_hash(data) + type = data[:type] || data["type"] || "" + location = Location.from_hash(data[:location] || data["location"]) + message = data[:message] || data["message"] || "" + <%- error.fields.each do |field| -%> + <%- case field -%> + <%- when Herb::Template::StringField -%> + <%= field.name %> = data[:<%= field.name %>] || data["<%= field.name %>"] || "" + <%- when Herb::Template::TokenField -%> + <%= field.name %> = data[:<%= field.name %>] ? Herb::Token.from_hash(data[:<%= field.name %>] || data["<%= field.name %>"]) : nil + <%- else -%> + <%= field.name %> = data[:<%= field.name %>] || data["<%= field.name %>"] + <%- end -%> + <%- end -%> + + new(type, location, message, <%= error.fields.map(&:name).join(", ") %>) + end + #: (?Integer) -> String def tree_inspect(indent = 0) output = +"" - output += %(@ #{error_name} #{location.tree_inspect}\n) + output += %(@ #{error_name} #{location&.tree_inspect || "?"}\n) <%- symbol = error.fields.none? ? "└──" : "├──" -%> output += %(<%= symbol %> message: #{message.inspect}\n) <%- error.fields.each do |field| -%> diff --git a/templates/template.rb b/templates/template.rb index 4029554bd..7ad8251e5 100755 --- a/templates/template.rb +++ b/templates/template.rb @@ -65,7 +65,7 @@ def c_type end def ruby_type - "Herb::AST::#{specific_kind || "Node"}" + "Herb::AST::#{specific_kind || "Node"}?" end def specific_kind @@ -79,7 +79,7 @@ def union_kind class TokenField < Field def ruby_type - "Herb::Token" + "Herb::Token?" end def c_type diff --git a/test/backend_format_lint_test.rb b/test/backend_format_lint_test.rb new file mode 100644 index 000000000..5c655f088 --- /dev/null +++ b/test/backend_format_lint_test.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class BackendFormatLintTest < Minitest::Test + def setup + @template = <<~ERB.strip +
+ <% if user.logged_in? %> +

Welcome, <%= user.name %>!

+ <% end %> +
+ ERB + + @messy_template = <<~ERB.strip +
+ <%if user.logged_in?%> +

Welcome, <%= user.name %>!

+ <%end%> +
+ ERB + end + + def test_native_backend_format_uses_default_node_backend + Herb.switch_backend(:native) + + skip "Node backend not available" unless node_backend_available? + + result = Herb.format(@messy_template) + assert_instance_of String, result + rescue StandardError => e + assert_instance_of StandardError, e + end + + def test_native_backend_lint_uses_default_node_backend + Herb.switch_backend(:native) + + skip "Node backend not available" unless node_backend_available? + + result = Herb.lint(@template) + assert_instance_of Herb::LintResult, result + rescue StandardError => e + assert_instance_of StandardError, e + end + + def test_native_backend_format_file_uses_default_node_backend + require "tempfile" + + Herb.switch_backend(:native) + skip "Node backend not available" unless node_backend_available? + + Tempfile.create(["test", ".html.erb"]) do |file| + file.write(@messy_template) + file.flush + + result = Herb.format_file(file.path) + assert_instance_of String, result + end + rescue StandardError => e + assert_instance_of StandardError, e + end + + def test_native_backend_lint_file_uses_default_node_backend + require "tempfile" + + Herb.switch_backend(:native) + skip "Node backend not available" unless node_backend_available? + + Tempfile.create(["test", ".html.erb"]) do |file| + file.write(@template) + file.flush + + result = Herb.lint_file(file.path) + assert_instance_of Herb::LintResult, result + end + rescue StandardError => e + assert_instance_of StandardError, e + end + + def test_node_backend_format_functionality + skip "Node backend not available" unless node_backend_available? + + Herb.switch_backend(:node) + + assert_respond_to Herb, :format + assert_respond_to Herb, :format_file + + begin + result = Herb.format(@messy_template) + assert_instance_of String, result + rescue StandardError => e + assert_instance_of StandardError, e + end + end + + def test_node_backend_lint_functionality + skip "Node backend not available" unless node_backend_available? + + Herb.switch_backend(:node) + + assert_respond_to Herb, :lint + assert_respond_to Herb, :lint_file + + begin + result = Herb.lint(@template) + assert_instance_of Herb::LintResult, result + rescue StandardError => e + assert_instance_of StandardError, e + end + end + + def test_format_and_lint_methods_exist_on_available_backends + [:native, :node].each do |backend_name| + next unless backend_available?(backend_name) + + Herb.switch_backend(backend_name) + + assert_respond_to Herb, :format + assert_respond_to Herb, :format_file + assert_respond_to Herb, :lint + assert_respond_to Herb, :lint_file + + current_backend = Herb.backend(backend_name) + assert_respond_to current_backend, :format + assert_respond_to current_backend, :format_file + assert_respond_to current_backend, :lint + assert_respond_to current_backend, :lint_file + end + end + + private + + def backend_available?(backend_name) + case backend_name + when :native + true + when :node + node_backend_available? + else + false + end + end + + def node_backend_available? + require "nodo" + true + rescue LoadError + false + end +end diff --git a/test/backend_system_test.rb b/test/backend_system_test.rb new file mode 100644 index 000000000..f9187ea81 --- /dev/null +++ b/test/backend_system_test.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class BackendSystemTest < Minitest::Test + def setup + @original_backend = Herb.current_backend + end + + def teardown + Herb.switch_backend(@original_backend) if @original_backend + end + + def test_herb_has_format_and_lint_methods + assert_respond_to Herb, :format + assert_respond_to Herb, :format_file + assert_respond_to Herb, :lint + assert_respond_to Herb, :lint_file + end + + def test_backend_abstract_methods_are_defined + backend = Herb::Backend.new + backend.instance_variable_set(:@loaded, true) + + assert_raises(NotImplementedError) { backend.format("test", {}) } + assert_raises(NotImplementedError) { backend.lint("test", {}) } + end + + def test_backend_format_and_lint_methods_exist + backend = Herb::Backend.new + + assert_respond_to backend, :format + assert_respond_to backend, :format_file + assert_respond_to backend, :lint + assert_respond_to backend, :lint_file + end + + def test_backend_format_and_lint_require_loaded_backend + backend = Herb::Backend.new + + error = assert_raises(RuntimeError) do + backend.format("test") + end + + assert_includes error.message, "Backend not loaded" + + error = assert_raises(RuntimeError) do + backend.lint("test") + end + + assert_includes error.message, "Backend not loaded" + end + + def test_native_backend_has_helpful_error_messages + error = assert_raises(NotImplementedError) do + Herb.format("test", backend: :native) + end + + assert_includes error.message, "native backend" + assert_includes error.message, "Node backend" + assert_includes error.message, "switch_backend(:node)" + + error = assert_raises(NotImplementedError) do + Herb.lint("test", backend: :native) + end + + assert_includes error.message, "native backend" + assert_includes error.message, "Node backend" + assert_includes error.message, "switch_backend(:node)" + end + + def test_format_file_reads_from_filesystem + require "tempfile" + + Tempfile.create(["test", ".html.erb"]) do |file| + file.write("
") + file.flush + + error = assert_raises(NotImplementedError) do + Herb.format_file(file.path, backend: :native) + end + + assert_includes error.message, "Formatting is not implemented" + refute_includes error.message.downcase, "file" + refute_includes error.message.downcase, "read" + end + end + + def test_lint_file_reads_from_filesystem + require "tempfile" + + Tempfile.create(["test", ".html.erb"]) do |file| + file.write("
") + file.flush + + error = assert_raises(NotImplementedError) do + Herb.lint_file(file.path, backend: :native) + end + + assert_includes error.message, "Linting is not implemented" + refute_includes error.message.downcase, "file" + refute_includes error.message.downcase, "read" + end + end + + private + + def node_backend_available? + require "nodo" + true + rescue LoadError + false + end +end diff --git a/test/cli_test.rb b/test/cli_test.rb new file mode 100644 index 000000000..a8884fd42 --- /dev/null +++ b/test/cli_test.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require_relative "test_helper" +require "stringio" + +class CLITest < Minitest::Test + def setup + @cli_class = Herb::CLI + @original_stdout = $stdout + @original_stderr = $stderr + $stdout = StringIO.new + $stderr = StringIO.new + end + + def teardown + $stdout = @original_stdout + $stderr = @original_stderr + end + + def test_cli_class_exists + assert_equal Herb::CLI, @cli_class + end + + def test_cli_has_new_attributes + cli = @cli_class.new([]) + + assert_respond_to cli, :indent_width + assert_respond_to cli, :indent_width= + assert_respond_to cli, :max_line_length + assert_respond_to cli, :max_line_length= + end + + def test_help_includes_new_commands + cli = @cli_class.new(["help"]) + + cli.call + + assert_respond_to cli, :help + end + + def test_option_parser_includes_new_options + cli = @cli_class.new([]) + parser = cli.option_parser + + parser_help = parser.to_s + + assert_includes parser_help, "--indent-width" + assert_includes parser_help, "--max-line-length" + end + + def test_format_command_handler_exists + cli = @cli_class.new(["format"]) + + assert_includes cli.private_methods, :handle_format_command + end + + def test_lint_command_handler_exists + cli = @cli_class.new(["lint"]) + + assert_includes cli.private_methods, :handle_lint_command + end + + def test_format_and_lint_commands_in_help_text + @cli_class.new([]) + + require "tempfile" + + Tempfile.create(["test", ".html.erb"]) do |file| + file.write("
Hello World
") + file.flush + + format_cli = @cli_class.new(["format", file.path]) + assert_equal "format", format_cli.instance_variable_get(:@command) + + lint_cli = @cli_class.new(["lint", file.path]) + assert_equal "lint", lint_cli.instance_variable_get(:@command) + end + end + + def test_indent_width_option_parsing + cli = @cli_class.new(["format", "test.erb", "--indent-width", "4"]) + cli.options + + assert_equal 4, cli.indent_width + end + + def test_formatting_options_parsing + cli = @cli_class.new(["format", "test.erb", "--indent-width", "4", "--max-line-length", "120"]) + cli.options + + assert_equal 4, cli.indent_width + assert_equal 120, cli.max_line_length + end +end diff --git a/test/complete_format_lint_test.rb b/test/complete_format_lint_test.rb new file mode 100644 index 000000000..b58c1e988 --- /dev/null +++ b/test/complete_format_lint_test.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class CompleteFormatLintTest < Minitest::Test + def test_format_and_lint_functionality_summary + assert defined?(Herb::Diagnostic) + assert defined?(Herb::LintOffense) + assert defined?(Herb::LintResult) + + assert_respond_to Herb, :format + assert_respond_to Herb, :format_file + assert_respond_to Herb, :lint + assert_respond_to Herb, :lint_file + + diagnostic = Herb::Diagnostic.new( + message: "Test diagnostic", + location: create_test_location, + severity: "error" + ) + + assert_instance_of Herb::Diagnostic, diagnostic + assert diagnostic.error? + + lint_offense = Herb::LintOffense.new( + message: "Test lint offense", + location: create_test_location, + severity: "warning", + rule: "test-rule" + ) + + assert_instance_of Herb::LintOffense, lint_offense + assert_kind_of Herb::Diagnostic, lint_offense + assert lint_offense.warning? + assert_equal "test-rule", lint_offense.rule + end + + def test_lint_result_integration + error_offense = Herb::LintOffense.new( + message: "Missing alt attribute", + location: create_test_location, + severity: "error", + rule: "html-img-require-alt" + ) + + warning_offense = Herb::LintOffense.new( + message: "Consider semantic HTML", + location: create_test_location, + severity: "warning", + rule: "html-semantic" + ) + + lint_result = Herb::LintResult.new([error_offense, warning_offense]) + + assert_equal 2, lint_result.total_offenses + assert_equal 1, lint_result.errors + assert_equal 1, lint_result.warnings + assert_equal 0, lint_result.infos + assert_equal 0, lint_result.hints + + refute lint_result.success? + refute lint_result.clean? + + assert_equal 1, lint_result.error_offenses.length + assert_equal 1, lint_result.warning_offenses.length + assert_equal 1, lint_result.offenses_for_rule("html-img-require-alt").length + + hash = lint_result.to_h + assert_instance_of Hash, hash + assert_equal 2, hash[:total_offenses] + + json = lint_result.to_json + assert_instance_of String, json + assert_includes json, "Missing alt attribute" + + string_output = lint_result.to_s + assert_includes string_output, "2 lint offenses" + assert_includes string_output, "1 errors" + assert_includes string_output, "1 warnings" + end + + def test_backend_integration_with_lint_result + original_backend = Herb.current_backend + skip "Node backend not available" unless node_backend_available? + + begin + Herb.switch_backend(:native) + + result = Herb.lint("
") + + assert_instance_of Herb::LintResult, result + rescue StandardError => e + assert_instance_of StandardError, e + ensure + Herb.switch_backend(original_backend) if original_backend + end + end + + def test_javascript_data_conversion + offense_data = { + message: "Missing alt attribute", + severity: "error", + rule: "html-img-require-alt", + code: "E001", + location: { + start: { line: 1, column: 5 }, + end: { line: 1, column: 20 }, + }, + } + + offense = Herb::LintOffense.from_hash(offense_data) + + assert_equal "Missing alt attribute", offense.message + assert_equal "error", offense.severity + assert_equal "html-img-require-alt", offense.rule + assert_equal "E001", offense.code + assert_equal 1, offense.location.start.line + assert_equal 5, offense.location.start.column + + result_data = { + offenses: [offense_data], + } + + result = Herb::LintResult.from_hash(result_data) + + assert_equal 1, result.total_offenses + assert_equal 1, result.errors + assert_equal 0, result.warnings + refute result.success? + end + + private + + def create_test_location + start_pos = Herb::Position.new(1, 5) + end_pos = Herb::Position.new(1, 15) + Herb::Location.new(start_pos, end_pos) + end + + def node_backend_available? + require "nodo" + true + rescue LoadError + false + end +end diff --git a/test/cross_backend_test.rb b/test/cross_backend_test.rb new file mode 100644 index 000000000..41bec1e00 --- /dev/null +++ b/test/cross_backend_test.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class CrossBackendTest < Minitest::Test + def test_backend_method_gets_specific_backend + native_backend = Herb.backend(:native) + assert_instance_of Herb::Backends::NativeBackend, native_backend + assert native_backend.loaded? + + current = Herb.current_backend + Herb.backend(:native) + assert_equal current, Herb.current_backend + end + + def test_backend_kwarg_support + template = "
Hello World
" + + Herb.parse(template, backend: :native) + result = Herb.parse(template) + + assert result.value.respond_to?(:to_source) if result.success? + + assert true + end + + def test_backend_method_access + native_backend = Herb.backend(:native) + assert_instance_of Herb::Backends::NativeBackend, native_backend + + current = Herb.current_backend + Herb.backend(:native) + + assert_equal current, Herb.current_backend + end + + def test_cross_backend_usage_with_kwarg + skip "Node backend dependencies not available" unless node_backend_available? + + original_backend = Herb.current_backend + + begin + Herb.switch_backend(:native) + template = "
HelloWorld
" + result = Herb.parse(template) + + assert result.success? + assert_equal "native", Herb.current_backend + + formatted_output = Herb.format(template, backend: :node) + lint_result = Herb.lint(template, backend: :node) + + node_formatted = result.value.to_source(backend: :node) + node_identity = result.value.to_source(backend: :node) + + assert_equal "native", Herb.current_backend + + assert_instance_of String, formatted_output + assert_instance_of Herb::LintResult, lint_result + assert_instance_of String, node_formatted + assert_instance_of String, node_identity + + assert_includes formatted_output, "Hello" + assert_includes node_formatted, "Hello" + assert_includes node_identity, "Hello" + rescue Nodo::DependencyError + skip "Node.js packages not installed" + ensure + Herb.switch_backend(original_backend) if original_backend + end + end + + def test_backend_kwarg_functionality + skip "Node backend dependencies not available" unless node_backend_available? + + original_backend = Herb.current_backend + + begin + Herb.switch_backend(:native) + template = "
MessyHTML
" + + formatted = Herb.format(template, backend: :node) + lint_result = Herb.lint(template, backend: :node) + parsed = Herb.parse(template, backend: :native) + + assert_instance_of String, formatted + assert_instance_of Herb::LintResult, lint_result + assert_instance_of Herb::ParseResult, parsed + + assert_equal "native", Herb.current_backend + rescue Nodo::DependencyError + skip "Node.js packages not installed" + ensure + Herb.switch_backend(original_backend) if original_backend + end + end + + private + + def node_backend_available? + require "nodo" + true + rescue LoadError + false + end +end diff --git a/test/format_lint_summary_test.rb b/test/format_lint_summary_test.rb new file mode 100644 index 000000000..3c0aad777 --- /dev/null +++ b/test/format_lint_summary_test.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class FormatLintSummaryTest < Minitest::Test + def test_format_and_lint_methods_exist + assert_respond_to Herb, :format + assert_respond_to Herb, :format_file + assert_respond_to Herb, :lint + assert_respond_to Herb, :lint_file + end + + def test_format_lint_use_node_backend_by_default + Herb.switch_backend(:native) + + skip "Node backend not available" unless node_backend_available? + + begin + result = Herb.format("
") + assert_instance_of String, result + rescue StandardError => e + assert_instance_of StandardError, e + end + + begin + result = Herb.lint("
") + assert_instance_of Herb::LintResult, result + rescue StandardError => e + assert_instance_of StandardError, e + end + end + + def test_available_backends_returns_available_list + backends = Herb.available_backends + + assert_instance_of Array, backends + assert_includes backends, :native + + return unless node_backend_available? + + assert_includes backends, :node + end + + def test_node_backend_methods_exist_when_available + skip "Node backend dependencies not available" unless node_backend_available? + + begin + Herb.switch_backend(:node) + + assert_respond_to Herb, :format + assert_respond_to Herb, :lint + assert_respond_to Herb.backend(:node), :format + assert_respond_to Herb.backend(:node), :lint + rescue Nodo::DependencyError, StandardError + skip "Node packages not installed, but backend switching worked" + end + end + + def test_multi_backend_format_lint_behavior + original_backend = Herb.current_backend + + skip "Node backend not available" unless node_backend_available? + + begin + Herb.switch_backend(:native) + + result = Herb.format("
") + assert_instance_of String, result + + result = Herb.lint("
") + assert_instance_of Herb::LintResult, result + + Herb.switch_backend(:node) + + result = Herb.format("
") + assert_instance_of String, result + + result = Herb.lint("
") + assert_instance_of Herb::LintResult, result + rescue Nodo::DependencyError + skip "Node backend available but packages not installed" + ensure + Herb.switch_backend(original_backend) if original_backend + end + end + + private + + def node_backend_available? + require "nodo" + true + rescue LoadError + false + end +end diff --git a/test/lint_result_test.rb b/test/lint_result_test.rb new file mode 100644 index 000000000..03baf149e --- /dev/null +++ b/test/lint_result_test.rb @@ -0,0 +1,243 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class LintResultTest < Minitest::Test + def setup + @position = Herb::Position.new(1, 5) + @location = Herb::Location.new(@position, Herb::Position.new(1, 15)) + + @error_offense = Herb::LintOffense.new( + message: "Missing alt attribute", + location: @location, + severity: "error", + rule: "html-img-require-alt" + ) + + @warning_offense = Herb::LintOffense.new( + message: "Consider using semantic HTML", + location: @location, + severity: "warning", + rule: "html-semantic-elements" + ) + end + + def test_diagnostic_creation + diagnostic = Herb::Diagnostic.new( + message: "Test message", + location: @location, + severity: "error", + code: "E001", + source: "test" + ) + + assert_equal "Test message", diagnostic.message + assert_equal @location, diagnostic.location + assert_equal "error", diagnostic.severity + assert_equal "E001", diagnostic.code + assert_equal "test", diagnostic.source + assert diagnostic.error? + refute diagnostic.warning? + end + + def test_diagnostic_severity_validation + error = assert_raises(ArgumentError) do + Herb::Diagnostic.new( + message: "Test", + location: @location, + severity: "invalid" + ) + end + + assert_includes error.message, "Invalid severity" + assert_includes error.message, "error, warning, info, hint" + end + + def test_lint_offense_creation + offense = Herb::LintOffense.new( + message: "Test offense", + location: @location, + severity: "warning", + rule: "test-rule" + ) + + assert_equal "Test offense", offense.message + assert_equal "warning", offense.severity + assert_equal "test-rule", offense.rule + assert_equal "linter", offense.source + assert offense.warning? + end + + def test_lint_offense_from_hash + data = { + message: "Missing alt attribute", + severity: "error", + rule: "html-img-require-alt", + location: { + start: { line: 1, column: 5 }, + end: { line: 1, column: 15 }, + }, + } + + offense = Herb::LintOffense.from_hash(data) + + assert_equal "Missing alt attribute", offense.message + assert_equal "error", offense.severity + assert_equal "html-img-require-alt", offense.rule + assert_equal 1, offense.location.start.line + assert_equal 5, offense.location.start.column + end + + def test_lint_offense_from_hash_with_minimal_data + data = { + message: "Test message", + } + + offense = Herb::LintOffense.from_hash(data) + + assert_equal "Test message", offense.message + assert_equal "error", offense.severity + assert_equal "unknown", offense.rule + assert_equal 1, offense.location.start.line + end + + def test_lint_result_creation + result = Herb::LintResult.new([@error_offense, @warning_offense]) + + assert_equal 2, result.total_offenses + assert_equal 1, result.errors + assert_equal 1, result.warnings + assert_equal 0, result.infos + assert_equal 0, result.hints + + refute result.success? + refute result.clean? + end + + def test_lint_result_empty + result = Herb::LintResult.new([]) + + assert_equal 0, result.total_offenses + assert_equal 0, result.errors + assert_equal 0, result.warnings + + assert result.success? + assert result.clean? + end + + def test_lint_result_success_with_warnings + result = Herb::LintResult.new([@warning_offense]) + + assert_equal 1, result.total_offenses + assert_equal 0, result.errors + assert_equal 1, result.warnings + + assert result.success? + refute result.clean? + end + + def test_lint_result_offense_filtering + info_offense = Herb::LintOffense.new( + message: "Info message", + location: @location, + severity: "info", + rule: "info-rule" + ) + + result = Herb::LintResult.new([@error_offense, @warning_offense, info_offense]) + + assert_equal 1, result.error_offenses.length + assert_equal 1, result.warning_offenses.length + assert_equal 1, result.info_offenses.length + assert_equal 0, result.hint_offenses.length + + assert_equal @error_offense, result.error_offenses.first + assert_equal @warning_offense, result.warning_offenses.first + end + + def test_lint_result_offense_by_rule + other_error = Herb::LintOffense.new( + message: "Another error", + location: @location, + severity: "error", + rule: "html-img-require-alt" + ) + + result = Herb::LintResult.new([@error_offense, @warning_offense, other_error]) + + img_offenses = result.offenses_for_rule("html-img-require-alt") + assert_equal 2, img_offenses.length + + semantic_offenses = result.offenses_for_rule("html-semantic-elements") + assert_equal 1, semantic_offenses.length + + nonexistent_offenses = result.offenses_for_rule("nonexistent-rule") + assert_equal 0, nonexistent_offenses.length + end + + def test_lint_result_from_hash + data = { + offenses: [ + { + message: "Missing alt attribute", + severity: "error", + rule: "html-img-require-alt", + location: { + start: { line: 1, column: 5 }, + end: { line: 1, column: 15 }, + }, + }, + { + message: "Consider semantic HTML", + severity: "warning", + rule: "html-semantic-elements", + } + ], + } + + result = Herb::LintResult.from_hash(data) + + assert_equal 2, result.total_offenses + assert_equal 1, result.errors + assert_equal 1, result.warnings + + error_offense = result.error_offenses.first + + assert_equal "Missing alt attribute", error_offense.message + assert_equal "html-img-require-alt", error_offense.rule + end + + def test_lint_result_to_s + clean_result = Herb::LintResult.new([]) + assert_equal "✓ No lint offenses found", clean_result.to_s + + result = Herb::LintResult.new([@error_offense, @warning_offense]) + result_string = result.to_s + + assert_includes result_string, "✗ Found 2 lint offenses" + assert_includes result_string, "1 errors" + assert_includes result_string, "1 warnings" + end + + def test_position_from_hash + data = { line: 5, column: 10 } + position = Herb::Position.from_hash(data) + + assert_equal 5, position.line + assert_equal 10, position.column + end + + def test_location_from_hash + data = { + start: { line: 1, column: 5 }, + end: { line: 2, column: 15 }, + } + location = Herb::Location.from_hash(data) + + assert_equal 1, location.start.line + assert_equal 5, location.start.column + + assert_equal 2, location.end.line + assert_equal 15, location.end.column + end +end diff --git a/test/node_backend_integration_test.rb b/test/node_backend_integration_test.rb new file mode 100644 index 000000000..45da669db --- /dev/null +++ b/test/node_backend_integration_test.rb @@ -0,0 +1,248 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class NodeBackendIntegrationTest < Minitest::Test + def setup + skip "Node backend dependencies not available" unless node_backend_fully_available? + + @template = <<~ERB.strip +
+ <% if user.logged_in? %> +

Welcome, <%= user.name %>!

+ <% end %> +
+ ERB + + @messy_template = <<~ERB.strip +
+ <%if user.logged_in?%> +

Welcome, <%= user.name %>!

+ <%end%> +
+ ERB + + @template_with_issues = <<~ERB.strip +
+ <%if user.logged_in?%> +

Welcome, <%= user.name %>!

+ <%end%> +
+ ERB + + Herb.switch_backend(:node) + end + + def test_format_returns_string + skip_if_packages_missing + + result = Herb.format(@messy_template) + + assert_instance_of String, result + assert result.length.positive? + refute_equal @messy_template, result + end + + def test_format_improves_formatting + skip_if_packages_missing + + result = Herb.format(@messy_template) + + refute_includes result, "<%if" + refute_includes result, "<%end%>" + refute_includes result, "
") + rescue Nodo::DependencyError, StandardError => e + raise e unless e.message.include?("Cannot find package") + + skip "Node.js packages not installed: #{e.message.lines.first.strip}" + end +end diff --git a/test/parser/parser_test.rb b/test/parser/parser_test.rb index ff61e334f..18a37504f 100644 --- a/test/parser/parser_test.rb +++ b/test/parser/parser_test.rb @@ -39,8 +39,8 @@ class ParserTest < Minitest::Spec │ ├── tag_opening: "<%=" (location: (1:4)-(1:7)) │ ├── content: " RUBY_VERSION " (location: (1:7)-(1:21)) │ ├── tag_closing: "%>" (location: (1:21)-(1:23)) - │ ├── parsed: false - │ └── valid: false + │ ├── parsed: true + │ └── valid: true │ ├── close_tag: │ └── @ HTMLCloseTagNode (location: (1:23)-(1:28)) diff --git a/test/print_node_test.rb b/test/print_node_test.rb new file mode 100644 index 000000000..9fa09b759 --- /dev/null +++ b/test/print_node_test.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class PrintNodeTest < Minitest::Test + def test_node_to_source_method_exists + template = "
Hello World
" + result = Herb.parse(template) + + assert result.success? + assert_respond_to result.value, :to_source + end + + def test_native_backend_print_node_not_implemented + original_backend = Herb.current_backend + + begin + Herb.switch_backend(:native) + template = "
Hello World
" + result = Herb.parse(template) + + error = assert_raises(NotImplementedError) do + result.value.to_source(backend: :native) + end + + assert_includes error.message, "Node printing is not implemented in the native backend" + assert_includes error.message, "Node backend" + ensure + Herb.switch_backend(original_backend) if original_backend + end + end + + def test_node_backend_print_node_works + skip "Node backend dependencies not available" unless node_backend_available? + + original_backend = Herb.current_backend + + begin + Herb.switch_backend(:node) + template = "
Hello World
" + result = Herb.parse(template) + + assert result.success? + + erb_output = result.value.to_source + + assert_instance_of String, erb_output + assert erb_output.length.positive? + + assert_includes erb_output, "div" + assert_includes erb_output, "Hello World" + rescue Nodo::DependencyError + skip "Node.js packages not installed" + ensure + Herb.switch_backend(original_backend) if original_backend + end + end + + def test_node_backend_format_option + skip "Node backend dependencies not available" unless node_backend_available? + + original_backend = Herb.current_backend + + begin + Herb.switch_backend(:node) + messy_template = "
HelloWorld
" + result = Herb.parse(messy_template) + + assert result.success? + + source_default = result.value.to_source + source_identity = result.value.to_source(format: false) + + assert_instance_of String, source_default + assert_instance_of String, source_identity + + assert_includes source_default, "div" + assert_includes source_default, "span" + assert_includes source_identity, "div" + assert_includes source_identity, "span" + assert_includes source_identity, "Hello" + + assert source_default.length.positive? + assert source_identity.length.positive? + rescue Nodo::DependencyError + skip "Node.js packages not installed" + ensure + Herb.switch_backend(original_backend) if original_backend + end + end + + private + + def node_backend_available? + require "nodo" + true + rescue LoadError + false + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 164c52243..ace408d97 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -14,6 +14,24 @@ Minitest::Spec::DSL.send(:alias_method, :test, :it) Minitest::Spec::DSL.send(:alias_method, :xtest, :xit) +class Minitest::Test + def setup + super + + @__original_herb_backend = Herb.current_backend + + Herb.switch_backend(:native) unless Herb.current_backend == :native + end + + def teardown + if @__original_herb_backend && @__original_herb_backend != Herb.current_backend + Herb.switch_backend(@__original_herb_backend) + end + + super + end +end + def cyclic_string(length) sequence = ("a".."z").to_a + ("0".."9").to_a sequence.cycle.take(length).join