foxtail-tools 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +19 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +66 -0
  5. data/exe/foxtail +12 -0
  6. data/lib/foxtail/cli/commands/check.rb +60 -0
  7. data/lib/foxtail/cli/commands/dump.rb +43 -0
  8. data/lib/foxtail/cli/commands/ids.rb +73 -0
  9. data/lib/foxtail/cli/commands/tidy.rb +107 -0
  10. data/lib/foxtail/cli.rb +59 -0
  11. data/lib/foxtail/syntax/error.rb +8 -0
  12. data/lib/foxtail/syntax/parser/ast/annotation.rb +23 -0
  13. data/lib/foxtail/syntax/parser/ast/attribute.rb +23 -0
  14. data/lib/foxtail/syntax/parser/ast/base_comment.rb +19 -0
  15. data/lib/foxtail/syntax/parser/ast/base_literal.rb +24 -0
  16. data/lib/foxtail/syntax/parser/ast/base_node.rb +89 -0
  17. data/lib/foxtail/syntax/parser/ast/call_arguments.rb +23 -0
  18. data/lib/foxtail/syntax/parser/ast/comment.rb +13 -0
  19. data/lib/foxtail/syntax/parser/ast/function_reference.rb +23 -0
  20. data/lib/foxtail/syntax/parser/ast/group_comment.rb +13 -0
  21. data/lib/foxtail/syntax/parser/ast/identifier.rb +19 -0
  22. data/lib/foxtail/syntax/parser/ast/junk.rb +23 -0
  23. data/lib/foxtail/syntax/parser/ast/message.rb +28 -0
  24. data/lib/foxtail/syntax/parser/ast/message_reference.rb +23 -0
  25. data/lib/foxtail/syntax/parser/ast/named_argument.rb +23 -0
  26. data/lib/foxtail/syntax/parser/ast/number_literal.rb +24 -0
  27. data/lib/foxtail/syntax/parser/ast/pattern.rb +22 -0
  28. data/lib/foxtail/syntax/parser/ast/placeable.rb +21 -0
  29. data/lib/foxtail/syntax/parser/ast/resource.rb +55 -0
  30. data/lib/foxtail/syntax/parser/ast/resource_comment.rb +13 -0
  31. data/lib/foxtail/syntax/parser/ast/select_expression.rb +23 -0
  32. data/lib/foxtail/syntax/parser/ast/span.rb +22 -0
  33. data/lib/foxtail/syntax/parser/ast/string_literal.rb +45 -0
  34. data/lib/foxtail/syntax/parser/ast/syntax_node.rb +22 -0
  35. data/lib/foxtail/syntax/parser/ast/term.rb +28 -0
  36. data/lib/foxtail/syntax/parser/ast/term_reference.rb +25 -0
  37. data/lib/foxtail/syntax/parser/ast/text_element.rb +19 -0
  38. data/lib/foxtail/syntax/parser/ast/variable_reference.rb +21 -0
  39. data/lib/foxtail/syntax/parser/ast/variant.rb +25 -0
  40. data/lib/foxtail/syntax/parser/ast.rb +12 -0
  41. data/lib/foxtail/syntax/parser/parse_error.rb +94 -0
  42. data/lib/foxtail/syntax/parser/stream.rb +338 -0
  43. data/lib/foxtail/syntax/parser.rb +797 -0
  44. data/lib/foxtail/syntax/serializer.rb +242 -0
  45. data/lib/foxtail/syntax/visitor.rb +61 -0
  46. data/lib/foxtail/syntax.rb +12 -0
  47. data/lib/foxtail/tools/error.rb +8 -0
  48. data/lib/foxtail/tools/version.rb +9 -0
  49. data/lib/foxtail-tools.rb +22 -0
  50. metadata +141 -0
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Regular comment node in FTL syntax (# comment)
8
+ class Comment < BaseComment
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents function calls with optional arguments
8
+ class FunctionReference < SyntaxNode
9
+ attr_accessor :id
10
+ attr_accessor :arguments
11
+
12
+ def initialize(id, arguments=nil)
13
+ super()
14
+ @id = id
15
+ @arguments = arguments
16
+ end
17
+
18
+ def children = [id, arguments].compact
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Group comment node in FTL syntax (## group comment)
8
+ class GroupComment < BaseComment
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents identifiers used for messages, terms, attributes, and function names
8
+ class Identifier < SyntaxNode
9
+ attr_accessor :name
10
+
11
+ def initialize(name)
12
+ super()
13
+ @name = name
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents unparseable content with associated error annotations
8
+ class Junk < SyntaxNode
9
+ attr_accessor :content
10
+ attr_accessor :annotations
11
+
12
+ def initialize(content, annotations=[])
13
+ super()
14
+ @content = content
15
+ @annotations = annotations
16
+ end
17
+
18
+ def children = annotations
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents a Fluent message with an identifier, optional value pattern,
8
+ # attributes, and an optional comment
9
+ class Message < SyntaxNode
10
+ attr_accessor :id
11
+ attr_accessor :value
12
+ attr_accessor :attributes
13
+ attr_accessor :comment
14
+
15
+ def initialize(id, value=nil, attributes=[], comment=nil)
16
+ super()
17
+ @id = id
18
+ @value = value
19
+ @attributes = attributes
20
+ @comment = comment
21
+ end
22
+
23
+ def children = [id, value, *attributes, comment].compact
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents references to messages with optional attribute access
8
+ class MessageReference < SyntaxNode
9
+ attr_accessor :id
10
+ attr_accessor :attribute
11
+
12
+ def initialize(id, attribute=nil)
13
+ super()
14
+ @id = id
15
+ @attribute = attribute
16
+ end
17
+
18
+ def children = [id, attribute].compact
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents named arguments in function calls (e.g., arg: value)
8
+ class NamedArgument < SyntaxNode
9
+ attr_accessor :name
10
+ attr_accessor :value
11
+
12
+ def initialize(name, value)
13
+ super()
14
+ @name = name
15
+ @value = value
16
+ end
17
+
18
+ def children = [name, value]
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents numeric literals (integers and floats)
8
+ class NumberLiteral < BaseLiteral
9
+ # Parse the number literal value and return as a Hash
10
+ # @return [Hash] Hash containing the parsed numeric value
11
+ def parse
12
+ value_str = @value
13
+
14
+ if value_str.include?(".")
15
+ {value: Float(value_str)}
16
+ else
17
+ {value: Integer(value_str, 10)}
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents a message or term value pattern consisting of text elements
8
+ # and placeables (expressions within braces)
9
+ class Pattern < SyntaxNode
10
+ attr_accessor :elements
11
+
12
+ def initialize(elements)
13
+ super()
14
+ @elements = elements
15
+ end
16
+
17
+ def children = elements
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents expressions within braces {} in a pattern that are evaluated at runtime
8
+ class Placeable < SyntaxNode
9
+ attr_accessor :expression
10
+
11
+ def initialize(expression)
12
+ super()
13
+ @expression = expression
14
+ end
15
+
16
+ def children = [expression]
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents a Fluent resource containing messages, terms, and comments
8
+ # This is the root node of a parsed Fluent file
9
+ class Resource < SyntaxNode
10
+ attr_accessor :body
11
+
12
+ def initialize(body=[])
13
+ super()
14
+ @body = body
15
+ end
16
+
17
+ def children = body
18
+
19
+ # Iterate over Message entries only
20
+ # @yield [Message] each message entry
21
+ # @return [Enumerator] if no block given
22
+ # @return [self] if block given
23
+ def each_message(&block)
24
+ return enum_for(__method__) unless block
25
+
26
+ body.each {|entry| yield(entry) if entry.is_a?(Message) }
27
+ self
28
+ end
29
+
30
+ # Iterate over Term entries only
31
+ # @yield [Term] each term entry
32
+ # @return [Enumerator] if no block given
33
+ # @return [self] if block given
34
+ def each_term(&block)
35
+ return enum_for(__method__) unless block
36
+
37
+ body.each {|entry| yield(entry) if entry.is_a?(Term) }
38
+ self
39
+ end
40
+
41
+ # Iterate over Message and Term entries (excludes comments and junk)
42
+ # @yield [Message, Term] each message or term entry
43
+ # @return [Enumerator] if no block given
44
+ # @return [self] if block given
45
+ def each_entry(&block)
46
+ return enum_for(__method__) unless block
47
+
48
+ body.each {|entry| yield(entry) if entry.is_a?(Message) || entry.is_a?(Term) }
49
+ self
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Resource comment node in FTL syntax (### resource comment)
8
+ class ResourceComment < BaseComment
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents select expressions for conditional message variants
8
+ class SelectExpression < SyntaxNode
9
+ attr_accessor :selector
10
+ attr_accessor :variants
11
+
12
+ def initialize(selector, variants)
13
+ super()
14
+ @selector = selector
15
+ @variants = variants
16
+ end
17
+
18
+ def children = [selector, *variants]
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents a source code span with start and end positions
8
+ # Used to track the location of AST nodes in the original source text
9
+ class Span < BaseNode
10
+ attr_accessor :start
11
+ attr_accessor :end
12
+
13
+ def initialize(start_pos, end_pos)
14
+ super()
15
+ @start = start_pos
16
+ @end = end_pos
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents quoted string literals with escape sequence processing
8
+ class StringLiteral < BaseLiteral
9
+ # Parse the string literal value, processing escape sequences
10
+ # Handles backslash escapes, Unicode escapes (uHHHH, UHHHHHH), and validates Unicode scalar values
11
+ # @return [Hash] Hash containing the parsed string value
12
+ def parse
13
+ # Backslash backslash, backslash double quote, uHHHH, UHHHHHH.
14
+ known_escapes = /(?:\\\\|\\"|\\u(\h{4})|\\U(\h{6}))/
15
+
16
+ escaped_value = @value.gsub(known_escapes) {|match|
17
+ codepoint4 = $1
18
+ codepoint6 = $2
19
+
20
+ case match
21
+ when "\\\\"
22
+ "\\"
23
+ when '\\"'
24
+ '"'
25
+ else
26
+ codepoint = (codepoint4 || codepoint6).to_i(16)
27
+ if codepoint <= 0xd7ff || 0xe000 <= codepoint
28
+ # It's a Unicode scalar value.
29
+ codepoint.chr(Encoding::UTF_8)
30
+ else
31
+ # Escape sequences representing surrogate code points are
32
+ # well-formed but invalid in Fluent. Replace them with U+FFFD
33
+ # REPLACEMENT CHARACTER.
34
+ "\uFFFD"
35
+ end
36
+ end
37
+ }
38
+
39
+ {value: escaped_value}
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Base class for AST nodes that can have span information
8
+ # Extends BaseNode with source position tracking capabilities
9
+ class SyntaxNode < BaseNode
10
+ attr_accessor :span
11
+
12
+ # Add span information to this syntax node
13
+ # @param start_pos [Integer] Starting position in the source
14
+ # @param end_pos [Integer] Ending position in the source
15
+ def add_span(start_pos, end_pos)
16
+ @span = Span.new(start_pos, end_pos)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents a Fluent term with an identifier, value pattern,
8
+ # optional attributes, and an optional comment
9
+ class Term < SyntaxNode
10
+ attr_accessor :id
11
+ attr_accessor :value
12
+ attr_accessor :attributes
13
+ attr_accessor :comment
14
+
15
+ def initialize(id, value, attributes=[], comment=nil)
16
+ super()
17
+ @id = id
18
+ @value = value
19
+ @attributes = attributes
20
+ @comment = comment
21
+ end
22
+
23
+ def children = [id, value, *attributes, comment].compact
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents references to terms with optional attribute access and arguments
8
+ class TermReference < SyntaxNode
9
+ attr_accessor :id
10
+ attr_accessor :attribute
11
+ attr_accessor :arguments
12
+
13
+ def initialize(id, attribute=nil, arguments=nil)
14
+ super()
15
+ @id = id
16
+ @attribute = attribute
17
+ @arguments = arguments
18
+ end
19
+
20
+ def children = [id, attribute, arguments].compact
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents plain text content within a pattern
8
+ class TextElement < SyntaxNode
9
+ attr_accessor :value
10
+
11
+ def initialize(value)
12
+ super()
13
+ @value = value
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents references to variables passed as arguments (e.g., $variable)
8
+ class VariableReference < SyntaxNode
9
+ attr_accessor :id
10
+
11
+ def initialize(id)
12
+ super()
13
+ @id = id
14
+ end
15
+
16
+ def children = [id]
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ module AST
7
+ # Represents individual variants within select expressions
8
+ class Variant < SyntaxNode
9
+ attr_accessor :key
10
+ attr_accessor :value
11
+ attr_accessor :default
12
+
13
+ def initialize(key, value, default: false)
14
+ super()
15
+ @key = key
16
+ @value = value
17
+ @default = default
18
+ end
19
+
20
+ def children = [key, value]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ # Namespace module for abstract syntax tree node classes
7
+ # With Zeitwerk, individual node classes are automatically loaded when referenced
8
+ module AST
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtail
4
+ module Syntax
5
+ class Parser
6
+ # Parse error with detailed error codes and messages
7
+ class ParseError < Foxtail::Syntax::Error
8
+ # @return [String] Error code (e.g., "E0001", "E0002")
9
+ attr_reader :code
10
+ # @return [Array] Additional arguments for error message formatting
11
+ attr_reader :args
12
+
13
+ # @param code [String] Error code
14
+ # @param args [Array] Additional arguments for error message formatting
15
+ def initialize(code, *args)
16
+ @code = code
17
+ @args = args
18
+ super(error_message(code, args))
19
+ end
20
+
21
+ private def error_message(code, args)
22
+ case code
23
+ when "E0001"
24
+ "Generic error"
25
+ when "E0002"
26
+ "Expected an entry start"
27
+ when "E0003"
28
+ token = args[0]
29
+ "Expected token: \"#{token}\""
30
+ when "E0004"
31
+ range = args[0]
32
+ "Expected a character from range: \"#{range}\""
33
+ when "E0005"
34
+ id = args[0]
35
+ "Expected message \"#{id}\" to have a value or attributes"
36
+ when "E0006"
37
+ id = args[0]
38
+ "Expected term \"-#{id}\" to have a value"
39
+ when "E0007"
40
+ "Expected a keyword"
41
+ when "E0008"
42
+ "The callee has to be an upper-case identifier or a term"
43
+ when "E0009"
44
+ "The argument name has to be a simple identifier"
45
+ when "E0010"
46
+ "Expected one of the variants to be marked as default (*)"
47
+ when "E0011"
48
+ "Expected at least one variant after \"->\""
49
+ when "E0012"
50
+ "Expected value"
51
+ when "E0013"
52
+ "Expected a variant key"
53
+ when "E0014"
54
+ "Expected literal"
55
+ when "E0015"
56
+ "Only one variant can be marked as default (*)"
57
+ when "E0016"
58
+ "Message references cannot be used as selectors"
59
+ when "E0017"
60
+ "Terms cannot be used as selectors"
61
+ when "E0018"
62
+ "Attributes of messages cannot be used as selectors"
63
+ when "E0019"
64
+ "Attributes of terms cannot be used as placeables"
65
+ when "E0020"
66
+ "Unterminated string expression"
67
+ when "E0021"
68
+ "Positional arguments must not follow named arguments"
69
+ when "E0022"
70
+ "Named arguments must be unique"
71
+ when "E0023"
72
+ "Expected an option list"
73
+ when "E0024"
74
+ "Expected a keyword argument"
75
+ when "E0025"
76
+ arg = args[0]
77
+ "Unknown escape sequence: \\#{arg}."
78
+ when "E0026"
79
+ sequence = args[0]
80
+ "Invalid Unicode escape sequence: #{sequence}."
81
+ when "E0027"
82
+ "Unbalanced closing brace in TextElement."
83
+ when "E0028"
84
+ "Expected an inline expression"
85
+ when "E0029"
86
+ "Nested placeables are not allowed"
87
+ else
88
+ "Unknown error: #{code}"
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end