Class: PartializerHelper

Inherits:
Object show all
Includes:
Partials
Defined in:
lib/ceedling/partials/partializer_helper.rb

Constant Summary

Constants included from Partials

Partials::ACCUMULATE, Partials::DEDUCT, Partials::MOCK_PRIVATE, Partials::MOCK_PUBLIC, Partials::PRIVATE, Partials::PUBLIC, Partials::TEST_PRIVATE, Partials::TEST_PUBLIC

Instance Method Summary collapse

Methods included from Partials

manufacture_function_declaration, manufacture_function_definition

Instance Method Details

#associate_function_line_numbers(name:, funcs:, filepath:, fallback:) ⇒ Object

Associate each FunctionDefinition with its line number in the original source file.

C source files are run through the GCC preprocessor before extraction. The resulting fully expanded output file retains GCC line markers of the form:

# <linenum> "<filename>" [flags]

These markers preserve the correspondence between preprocessed text and the original source lines, even after macro expansion has altered the content.

For each function in funcs, this method searches the preprocessor expansion for an exact match of the function's code_block`` text. When a match is found, the GCC line marker immediately preceding the match is used to calculate the 1-indexed source line number. This line number is then written back into the FunctionDefinition` struct alongside the originating filepath.

FunctionDefinition entries whose code_block cannot be located in the preprocessor output are skipped -- line_num fields remain unset.

source_filepath is always updated.

the preprocessor expansion file for that test context. locations are to be resolved. Matched entries are mutated in place. preprocessed, written into each matched FunctionDefinition as source_filepath. instead of preprocessed output (because preprocessed output is not available)

Parameters:

  • name (String)

    Name of the containing test, used to construct the path to

  • funcs (Array<FunctionDefinition>)

    Function definitions whose source

  • filepath (String)

    Path to the original C source file that was

  • fallback (bool)

    Whether to immediately use simple source file scanning



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/ceedling/partials/partializer_helper.rb', line 100

def associate_function_line_numbers(name:, funcs:, filepath:, fallback:)
  # File path of directives-only preprocessor output
  preprocessed_filepath = @file_path_utils.form_preprocessed_file_raw_directives_only_filepath( filepath, name )

  @utils.stamp_source_filepaths( funcs, filepath )

  if fallback
    msg = "Using fallback C function location search for #{filepath}"
    @loginator.log( msg, Verbosity::OBNOXIOUS, LogLabels::WARNING )

    funcs.each do |func|
      func.line_num = @utils.locate_function_in_source(
        code_block:  func.code_block,
        filepath:    filepath
      )
    end

  else
    funcs.each do |func|
      # Uses `locate_function_in_source` as an automatic fallback
      func.line_num = @utils.locate_function_via_preprocessed(
        code_block:            func.code_block,
        filepath:              filepath,
        preprocessed_filepath: preprocessed_filepath
      )
    end
  end

  funcs.each do |func|
    if func.line_num.nil?
      msg = "Could not locate function #{func.name}() in #{filepath} ➡️ Any test coverage reporting will be incomplete."
      @loginator.log( msg, Verbosity::COMPLAIN )
    end
  end

  header = "Found functions at line numbers in #{filepath}:"
  @loginator.log_list( @utils.format_line_number_list( funcs ), header, Verbosity::DEBUG )
end

#extract_function_scope_static_vars(funcs, name:, module_name:, file_type:) ⇒ Array<CVariableDeclaration>

Excise function-scoped static variable declarations from function bodies (to be promoted to module-scope).

C functions may contain local static variable declarations. These variables have file-level storage duration but function-level scope. When generating partials, they must be lifted out of function bodies and treated as module-level variables so that linker and coverage tooling see them correctly.

For each function in funcs, this method:

1. Scans the function body for variable declarations bearing a private keyword
 (i.e. any keyword in `CExtractorConstants::PRIVATE_KEYWORDS`, e.g. `static`).
2. Replaces each such declaration in the function's `code_block` and `body` with a
 no-op expression of the form `(void)0; /* <original text> */` so that coverage
 line mappings remain valid without re-declaring the variable inside the body.
3. Renames each private function-scoped declaration to be prepended with the 
 containing function name to prevent name collisions at module-scope.
4. Collects all promoted declarations and returns them for inclusion at module scope.

Parameters:

  • funcs (Array<CFunctionDefinition>)

    Function definitions to scan. Each matched function's code_block and body fields are mutated in place.

  • name (String)

    Test name, used in log messages.

  • module_name (String)

    Module name, used in log messages.

  • file_type (String)

    "source" or "header", used in log messages.

Returns:

  • (Array<CVariableDeclaration>)

    All function-scoped static variable declarations found across all supplied functions, suitable for emission at module scope.



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/ceedling/partials/partializer_helper.rb', line 165

def extract_function_scope_static_vars(funcs, name:, module_name:, file_type:)
  decls = []

  # Process each function definition looking for function-scoped static variables.
  # If found, collect them and remove from function `body` and `code_block`.
  funcs.each do |func|
    # Remove containing brackets of function body
    func_body = func.body.dup
    func_body.delete_prefix!( '{' )
    func_body.delete_suffix!( '}' )

    scanner = StringScanner.new( func_body )
    _decls = []

    loop do
      # `try_extract_variable` returns an array of declarations.
      # A compound declaration (e.g. int x, y) yields multiple declaration Structs
      success, var_decls = @declaration_extractor.try_extract_variable( scanner )
      break unless success
      var_decls.each do |var|
        if var.decorators.any? { |d| CExtractorConstants::PRIVATE_KEYWORDS.include?(d) }
          _decls << var
        end
      end
    end

    # Group declarations by original statement.
    # Simple declarations (one var per unique original) and 
    # compound declarations (multiple vars sharing the same original, e.g. `static int a, b;`)
    # require different strategies to prevent the restored comment text from being found and
    # corrupted by a subsequent replace call.
    groups = _decls.group_by { |var| var.original.strip }

    groups.each do |_original, vars|
      # Pre-compute old/new names before any mutation of var.name
      old_names   = vars.map(&:name)
      new_names   = old_names.map { |n| "partial_#{func.name}_#{n}" }

      # Single placeholder per group — keyed on the first variable's name
      placeholder = "__CEEDLING_NOOP_#{func.name.upcase}_#{old_names.first.upcase}__"

      # Replace original declaration: one no-op per variable, single comment with placeholder.
      # Defer placeholder restoration until after ALL renames are complete so that
      # restored comment text cannot be found and re-processed by a subsequent replace.
      if vars.size > 1
        func.code_block = @utils.replace_compound_declaration_with_noops( func.code_block, _original, placeholder, vars.size )
        func.body       = @utils.replace_compound_declaration_with_noops( func.body,       _original, placeholder, vars.size )
      else
        func.code_block = @utils.replace_declaration_with_noop( func.code_block, _original, placeholder )
        func.body       = @utils.replace_declaration_with_noop( func.body,       _original, placeholder )
      end

      # Rename all variables' token-bounded references (shared logic for both cases)
      vars.zip( old_names, new_names ).each do |var, old_name, new_name|
        var.text        = @utils.rename_c_identifier( var.text,        old_name, new_name )
        var.name        = new_name
        func.code_block = @utils.rename_c_identifier( func.code_block, old_name, new_name )
        func.body       = @utils.rename_c_identifier( func.body,       old_name, new_name )
      end

      # Restore original declaration text in the single placeholder comment
      func.code_block = func.code_block.sub(placeholder) { _original }
      func.body       = func.body.sub(placeholder)       { _original }
    end

    _decls_log = _decls.map { |d| "`#{d.original.strip}`" }
    @loginator.log_list(
      _decls_log,
      "Function-scope static variables in #{func.name}() in #{file_type} to be promoted for Partial #{name}::#{module_name}:",
      Verbosity::OBNOXIOUS
    ) unless _decls_log.empty?

    decls += _decls
  end

  return decls
end

#filter_and_transform_funcs(funcs, visibility, output_type) ⇒ Object

  1. Filter functions by visibility (:private | :public | :deduct) or seed an empty list (:accumulate)
  2. Transform functions to appropriate container (:impl | :interface) → FunctionDefinition[] or FunctionDeclaration[]


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/ceedling/partials/partializer_helper.rb', line 36

def filter_and_transform_funcs(funcs, visibility, output_type)
  # DEDUCT starts with all functions regardless of visibility; subtractions are applied later by the caller
  if visibility == DEDUCT
    return funcs.filter_map { |func| @utils.transform_function(func, func.signature_stripped, output_type) }
  end

  # ACCUMULATE starts with an empty list; the caller then injects explicitly named additions
  return [] unless [PRIVATE, PUBLIC].include?(visibility)

  # PUBLIC or PRIVATE: filter to matching visibility, then transform
  funcs.filter_map do |func|
    next unless @utils.matches_visibility?(func.decorators, visibility)

    @utils.transform_function(func, func.signature_stripped, output_type)
  end
end

#find_and_transform_func(name:, primary_funcs:, secondary_funcs:, output_type:) ⇒ Object

Find a function by name, searching primary_funcs first then secondary_funcs. Returns the first match transformed for the given output_type, or nil if not found. For :impl output_type, pass secondary_funcs: [] — declarations have no code_block.



56
57
58
59
60
61
62
63
64
# File 'lib/ceedling/partials/partializer_helper.rb', line 56

def find_and_transform_func(name:, primary_funcs:, secondary_funcs:, output_type:)
  func = primary_funcs.find { |f| f.name == name }
  return @utils.transform_function(func, func.signature_stripped, output_type) if func

  func = secondary_funcs.find { |f| f.name == name }
  return @utils.transform_function(func, func.signature_stripped, output_type) if func

  nil
end

#setupObject



28
29
30
31
32
# File 'lib/ceedling/partials/partializer_helper.rb', line 28

def setup()
  # Aliases
  @utils = @partializer_utils
  @declaration_extractor = @c_extractor_declarations
end

#subtract_funcs(funcs:, names:) ⇒ Object

Return funcs with any function whose name is in names removed.



67
68
69
70
71
# File 'lib/ceedling/partials/partializer_helper.rb', line 67

def subtract_funcs(funcs:, names:)
  return funcs if names.empty?
  name_set = Set.new(names)
  funcs.reject { |f| name_set.include?(f.name) }
end

#update_signatures_from_full_expansion(funcs:, full_expansion_filepath:, name:, module_name:, file_type:) ⇒ Object

For each function definition in funcs, find the matching function by name in the fully preprocessed expansion file and replace the three signature-related fields with their macro-expanded equivalents. All other fields — especially code_block — are preserved from the directives-only extraction so that line number mapping stays valid.

Parameters:

  • funcs (Array<CFunctionDefinition>)

    Definitions to update in place.

  • full_expansion_filepath (String)

    Path to the assembled full expansion file produced by preprocess_partial_header,source_expand_macros().

  • name (String)

    Test name, used in log messages.

  • module_name (String)

    Partial module name, used in log messages.

  • file_type (String)

    "source" or "header", used in log messages.



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/ceedling/partials/partializer_helper.rb', line 254

def update_signatures_from_full_expansion(funcs:, full_expansion_filepath:, name:, module_name:, file_type:)
  expanded_module  = @c_extractor.from_file( full_expansion_filepath )
  expanded_by_name = expanded_module.function_definitions.each_with_object({}) { |f, h| h[f.name] = f }

  updated = []
  funcs.each do |func|
    expanded = expanded_by_name[func.name]
    next unless expanded

    func.signature          = expanded.signature
    func.decorators         = expanded.decorators
    func.signature_stripped = expanded.signature_stripped
    updated << func.name
  end

  @loginator.log_list(
    updated.map { |n| "`#{n}`" },
    "Updated signatures from full expansion for Partial #{name}::#{module_name} #{file_type}:",
    Verbosity::DEBUG
  ) unless updated.empty?
end

#validate_additions_subtractions_visibility(c_module, config, name) ⇒ Object

Validate that subtractions match their type's own visibility classification. For PUBLIC type: subtractions must be public functions. For PRIVATE type: subtractions must be private functions. Additions are not validated (same-visibility additions are redundant but harmless). ACCUMULATE is skipped (no subtractions allowed, already enforced by extract_configs).



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/ceedling/partials/partializer_helper.rb', line 322

def validate_additions_subtractions_visibility(c_module, config, name)
  func_map = c_module.function_definitions.each_with_object({}) { |f, h| h[f.name] = f }
  mod      = config.module

  {tests: config.tests, mocks: config.mocks}.each do |label, pf|
    next unless pf.type == PUBLIC || pf.type == PRIVATE

    subtraction_required_visibility = pf.type

    pf.subtractions.each do |func_name|
      func = func_map[func_name]
      unless @utils.matches_visibility?(func.decorators, subtraction_required_visibility)
        raise CeedlingException.new(
          "#{name}: Partial configuration for module '#{mod}': #{label} type is #{pf.type}, " \
          "so subtractions must be #{subtraction_required_visibility} functions, " \
          "but '#{func_name}' is not #{subtraction_required_visibility}"
        )
      end
    end
  end
end

#validate_function_names_exist(c_module, config, name) ⇒ Object

Validate that every function name in additions and subtractions exists in c_module. Case-sensitive match; if a name matches case-insensitively but not exactly, raise a specific case-mismatch exception.



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/ceedling/partials/partializer_helper.rb', line 279

def validate_function_names_exist(c_module, config, name)
  known_exact = Set.new( c_module.function_definitions.map(&:name) )
  known_lower = Set.new( c_module.function_definitions.map { |f| f.name.downcase } )
  mod         = config.module

  {TEST: config.tests, MOCK: config.mocks}.each do |label, pf|
    (pf.additions + pf.subtractions).each do |func_name|
      next if known_exact.include?(func_name)

      if known_lower.include?(func_name.downcase)
        raise CeedlingException.new(
          "#{name}: #{label} Partial configuration for module '#{mod}' references function '#{func_name}' " \
          "which differs only by case from a real function name"
        )
      else
        raise CeedlingException.new(
          "#{name}: #{label} Partial configuration for module '#{mod}' references function '#{func_name}' which does not exist"
        )
      end
    end
  end
end

#validate_no_additions_subtractions_overlap(config, name) ⇒ Object

Validate that no function name appears in both additions and subtractions of the same tests or mocks entry.



304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/ceedling/partials/partializer_helper.rb', line 304

def validate_no_additions_subtractions_overlap(config, name)
  mod = config.module

  {TEST: config.tests, MOCK: config.mocks}.each do |label, pf|
    overlap = Set.new(pf.additions) & Set.new(pf.subtractions)
    overlap.each do |func_name|
      raise CeedlingException.new(
        "#{name}: #{label} Partial configuration for module '#{mod}' ⏩️ Function '#{func_name}' should not be both added and subtracted"
      )
    end
  end
end