Class: Jsonc::Merge::FileAnalysis

Inherits:
Object
  • Object
show all
Includes:
Ast::Merge::FileAnalyzable
Defined in:
lib/jsonc/merge/file_analysis.rb

Overview

Analyzes JSON/JSONC file structure, extracting nodes, comments, and freeze blocks.
This is the main analysis class that prepares JSON content for merging.

Supports JSONC (JSON with Comments) which allows single-line (//) and
multi-line (/* */) comments in JSON files. This is commonly used in
configuration files like tsconfig.json, VS Code settings, etc.

Examples:

Basic usage

analysis = FileAnalysis.new(json_source)
analysis.valid? # => true
analysis.nodes # => [NodeWrapper, FreezeNodeBase, ...]
analysis.freeze_blocks # => [FreezeNodeBase, ...]

Constant Summary collapse

DEFAULT_FREEZE_TOKEN =

Default freeze token for identifying freeze blocks

"json-merge"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source, freeze_token: DEFAULT_FREEZE_TOKEN, signature_generator: nil, parser_path: nil, **options) ⇒ FileAnalysis

Initialize file analysis

Parameters:

  • source (String)

    JSON/JSONC source code to analyze

  • freeze_token (String) (defaults to: DEFAULT_FREEZE_TOKEN)

    Token for freeze block markers

  • signature_generator (Proc, nil) (defaults to: nil)

    Custom signature generator

  • parser_path (String, nil) (defaults to: nil)

    Path to tree-sitter-json parser library

  • options (Hash)

    Additional options (forward compatibility - ignored by FileAnalysis)



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/jsonc/merge/file_analysis.rb', line 50

def initialize(source, freeze_token: DEFAULT_FREEZE_TOKEN, signature_generator: nil, parser_path: nil, **options)
  @source = source
  @lines = source.lines.map(&:chomp)
  @freeze_token = freeze_token
  @signature_generator = signature_generator
  @parser_path = parser_path || self.class.find_parser_path
  @errors = []
  # **options captures any additional parameters (e.g., node_typing) for forward compatibility

  # Initialize comment tracking
  @comment_tracker = CommentTracker.new(source)

  # Parse the JSON
  DebugLogger.time("FileAnalysis#parse_json") { parse_json }

  # Extract freeze blocks and integrate with nodes
  @freeze_blocks = extract_freeze_blocks
  @nodes = integrate_nodes_and_freeze_blocks

  DebugLogger.debug("FileAnalysis initialized", {
    signature_generator: signature_generator ? "custom" : "default",
    nodes_count: @nodes.size,
    freeze_blocks: @freeze_blocks.size,
    valid: valid?,
  })
end

Instance Attribute Details

#astTreeHaver::Tree? (readonly)

Returns Parsed AST.

Returns:

  • (TreeHaver::Tree, nil)

    Parsed AST



27
28
29
# File 'lib/jsonc/merge/file_analysis.rb', line 27

def ast
  @ast
end

#comment_trackerCommentTracker (readonly)

Returns Comment tracker for this file.

Returns:



24
25
26
# File 'lib/jsonc/merge/file_analysis.rb', line 24

def comment_tracker
  @comment_tracker
end

#errorsArray (readonly)

Returns Parse errors if any.

Returns:

  • (Array)

    Parse errors if any



30
31
32
# File 'lib/jsonc/merge/file_analysis.rb', line 30

def errors
  @errors
end

Class Method Details

.find_parser_pathString?

Find the parser library path using TreeHaver::GrammarFinder

Note: JSONC uses the tree-sitter-jsonc grammar (supports JSON with Comments)

Returns:

  • (String, nil)

    Path to the parser library or nil if not found



38
39
40
# File 'lib/jsonc/merge/file_analysis.rb', line 38

def find_parser_path
  TreeHaver::GrammarFinder.new(:jsonc).find_library_path
end

Instance Method Details

#fallthrough_node?(value) ⇒ Boolean

Override to detect tree-sitter nodes for signature generator fallthrough

Parameters:

  • value (Object)

    The value to check

Returns:

  • (Boolean)

    true if this is a fallthrough node



130
131
132
# File 'lib/jsonc/merge/file_analysis.rb', line 130

def fallthrough_node?(value)
  value.is_a?(NodeWrapper) || value.is_a?(FreezeNode) || super
end

#freeze_block_at(line_num) ⇒ FreezeNode?

Get the freeze block containing the given line.

Parameters:

  • line_num (Integer)

    1-based line number

Returns:



104
105
106
# File 'lib/jsonc/merge/file_analysis.rb', line 104

def freeze_block_at(line_num)
  @freeze_blocks.find { |fb| fb.location.cover?(line_num) }
end

#generate_signature(node) ⇒ Object

Override to handle special signature generation for root-level objects
When there’s only one root object, we want it to match regardless of keys
so that add_template_only_nodes can work properly



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/jsonc/merge/file_analysis.rb', line 111

def generate_signature(node)
  # If custom signature generator provided, let it handle everything
  return super if @signature_generator

  return super unless node.is_a?(NodeWrapper)
  return super unless node.object?

  # Check if this is the sole root object
  if statements.size == 1 && statements.first == node
    # Root object gets a consistent signature so they always match
    return [:root_object]
  end

  super
end

#in_freeze_block?(line_num) ⇒ Boolean

Check if a line is within a freeze block.

Parameters:

  • line_num (Integer)

    1-based line number

Returns:

  • (Boolean)


96
97
98
# File 'lib/jsonc/merge/file_analysis.rb', line 96

def in_freeze_block?(line_num)
  @freeze_blocks.any? { |fb| fb.location.cover?(line_num) }
end

#root_nodeNodeWrapper?

Get the root node of the parse tree

Returns:



136
137
138
139
140
# File 'lib/jsonc/merge/file_analysis.rb', line 136

def root_node
  return unless valid?

  NodeWrapper.new(@ast.root_node, lines: @lines, source: @source)
end

#root_objectNodeWrapper?

Get the root object if the JSON document is an object

Returns:



144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/jsonc/merge/file_analysis.rb', line 144

def root_object
  return unless valid?

  root = @ast.root_node
  return unless root

  # JSON root should be a document containing an object or array
  root.each do |child|
    if child.type.to_s == "object"
      return NodeWrapper.new(child, lines: @lines, source: @source)
    end
  end
  nil
end

#root_object_close_lineString?

Get the closing brace line of the root object (the line containing })

Returns:

  • (String, nil)


170
171
172
173
174
175
# File 'lib/jsonc/merge/file_analysis.rb', line 170

def root_object_close_line
  obj = root_object
  return unless obj&.end_line

  line_at(obj.end_line)&.chomp
end

#root_object_open_lineString?

Get the opening brace line of the root object (the line containing {)

Returns:

  • (String, nil)


161
162
163
164
165
166
# File 'lib/jsonc/merge/file_analysis.rb', line 161

def root_object_open_line
  obj = root_object
  return unless obj&.start_line

  line_at(obj.start_line)&.chomp
end

#root_pairsArray<NodeWrapper>

Get key-value pairs from the root object

Returns:



179
180
181
182
183
184
# File 'lib/jsonc/merge/file_analysis.rb', line 179

def root_pairs
  obj = root_object
  return [] unless obj

  obj.pairs
end

#statementsArray<NodeWrapper, FreezeNode> Also known as: nodes

The base module uses ‘statements’ - provide both names for compatibility

Returns:



85
86
87
# File 'lib/jsonc/merge/file_analysis.rb', line 85

def statements
  @nodes ||= []
end

#valid?Boolean

Check if parse was successful

Returns:

  • (Boolean)


79
80
81
# File 'lib/jsonc/merge/file_analysis.rb', line 79

def valid?
  @errors.empty? && !@ast.nil?
end