Class: PartializerHelper
- 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
-
#associate_function_line_numbers(name:, funcs:, filepath:, fallback:) ⇒ Object
Associate each FunctionDefinition with its line number in the original source file.
-
#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).
-
#filter_and_transform_funcs(funcs, visibility, output_type) ⇒ Object
-
#find_and_transform_func(name:, primary_funcs:, secondary_funcs:, output_type:) ⇒ Object
Find a function by name, searching primary_funcs first then secondary_funcs.
- #setup ⇒ Object
-
#subtract_funcs(funcs:, names:) ⇒ Object
Return funcs with any function whose name is in names removed.
-
#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.
-
#validate_additions_subtractions_visibility(c_module, config, name) ⇒ Object
Validate that subtractions match their type's own visibility classification.
-
#validate_function_names_exist(c_module, config, name) ⇒ Object
Validate that every function name in additions and subtractions exists in c_module.
-
#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.
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)
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.
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
- Filter functions by visibility (:private | :public | :deduct) or seed an empty list (:accumulate)
- Transform functions to appropriate container (:impl | :interface) →
FunctionDefinition[]orFunctionDeclaration[]
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 |
#setup ⇒ Object
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.
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:) = @c_extractor.from_file( full_expansion_filepath ) = .function_definitions.each_with_object({}) { |f, h| h[f.name] = f } updated = [] funcs.each do |func| = [func.name] next unless func.signature = .signature func.decorators = .decorators func.signature_stripped = .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 |