Class: CExtractorCodeText
- Includes:
- CExtractorConstants
- Defined in:
- lib/ceedling/c_extractor/c_extractor_code_text.rb
Constant Summary
Constants included from CExtractorConstants
CExtractorConstants::C11_SPECIFIER_KEYWORDS, CExtractorConstants::DECORATOR_KEYWORDS, CExtractorConstants::DEFAULT_CHUNK_SIZE, CExtractorConstants::DEFAULT_MAX_FUNCTION_LENGTH, CExtractorConstants::DEFAULT_MAX_LINE_LENGTH, CExtractorConstants::MODIFIER_KEYWORDS, CExtractorConstants::MSVC_CALLING_CONVENTIONS, CExtractorConstants::PRIVATE_KEYWORDS, CExtractorConstants::TYPE_KEYWORDS, CExtractorConstants::TYPE_QUALIFIER_KEYWORDS
Instance Method Summary collapse
-
#collect_balanced(scanner, open_char, close_char) ⇒ Array(Boolean, String|nil)
Collect the full text of a balanced delimiter pair starting AT open_char.
-
#extract_balanced_braces(scanner) ⇒ Array(Boolean, String|nil)
Extract a balanced block of braces from the scanner.
-
#skip_c_string(scanner, quote) ⇒ Object
Skip a C string or character literal Handles escape sequences to avoid false termination on escaped quotes.
- #skip_comment(scanner) ⇒ Object
-
#skip_compiler_extension(scanner) ⇒ Object
Skip a single compiler extension at the current scanner position.
-
#skip_deadspace(scanner) ⇒ Object
Skip "deadspace" - non-code elements that should be ignored during extraction Deadspace includes: - Whitespace (spaces, tabs, newlines, carriage returns) - Comments (both single-line // and multi-line /* */).
-
#skip_semicolons(scanner) ⇒ Object
Skip consecutive semicolons and any intervening deadspace.
-
#strip_compiler_extensions(text) ⇒ Object
Strip all compiler extensions from a string and return the cleaned result.
Instance Method Details
#collect_balanced(scanner, open_char, close_char) ⇒ Array(Boolean, String|nil)
Collect the full text of a balanced delimiter pair starting AT open_char. Nested pairs, string literals (verbatim), and comments (replaced with a single space) are handled correctly. Returns [true, text_including_delimiters] or [false, nil] on unbalanced input or EOS before the matching close delimiter is found.
Works for any single-character delimiter pair: '{}', '()', or '[]'.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/ceedling/c_extractor/c_extractor_code_text.rb', line 27 def collect_balanced(scanner, open_char, close_char) return [false, nil] unless scanner.peek(1) == open_char text = +scanner.getch # consume and record open_char depth = 1 until scanner.eos? ch = scanner.peek(1) if ch == '"' || ch == "'" before = scanner.pos skip_c_string(scanner, ch) text << scanner.string[before...scanner.pos] elsif scanner.check(%r{/[/*]}) skip_comment(scanner) text << ' ' elsif ch == open_char depth += 1 text << scanner.getch elsif ch == close_char depth -= 1 text << scanner.getch return [true, text] if depth == 0 else text << scanner.getch end end [false, nil] end |
#extract_balanced_braces(scanner) ⇒ Array(Boolean, String|nil)
Extract a balanced block of braces from the scanner. Delegates to collect_balanced() — see its documentation for full details.
63 64 65 |
# File 'lib/ceedling/c_extractor/c_extractor_code_text.rb', line 63 def extract_balanced_braces(scanner) collect_balanced(scanner, '{', '}') end |
#skip_c_string(scanner, quote) ⇒ Object
Skip a C string or character literal Handles escape sequences to avoid false termination on escaped quotes
Parameters:
scanner: StringScanner positioned at the opening quote
quote: The quote character (either '"' for strings or "'" for characters)
Returns: Number of bytes skipped (including opening and closing quotes)
Side effects: Advances scanner position past the closing quote (or to end of string if unterminated)
Examples:
"hello" -> skips 7 bytes
'a' -> skips 3 bytes
"say \"hi\"" -> skips 11 bytes (handles escaped quotes)
"path\\file" -> skips 11 bytes (handles escaped backslashes)
83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/ceedling/c_extractor/c_extractor_code_text.rb', line 83 def skip_c_string(scanner, quote) start_pos = scanner.pos scanner.getch # Opening quote until scanner.eos? if scanner.scan(/\\/) scanner.getch elsif scanner.getch == quote break end end return (scanner.pos - start_pos) end |
#skip_comment(scanner) ⇒ Object
184 185 186 187 188 189 190 191 192 |
# File 'lib/ceedling/c_extractor/c_extractor_code_text.rb', line 184 def skip_comment(scanner) # Single line comment if scanner.scan(%r{//}) scanner.skip_until(/\n/) || scanner.terminate # Multiline comment elsif scanner.scan(%r{/\*}) scanner.skip_until(%r{\*/}) || scanner.terminate end end |
#skip_compiler_extension(scanner) ⇒ Object
Skip a single compiler extension at the current scanner position. Uses collect_balanced() for paren matching so all nesting depths are handled. Returns true and advances the scanner if an extension is found; returns false without moving if the current position is not the start of a known compiler extension.
Handles:
__word__(…) — any double-underscore attribute form, e.g. __attribute__((…))
__declspec(…) — MSVC declaration specifier, including nested forms
Bare MSVC calling-convention keywords (__cdecl, __stdcall, etc.)
Does NOT skip __int64, __int32, or any non-extension __ identifiers.
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/ceedling/c_extractor/c_extractor_code_text.rb', line 205 def skip_compiler_extension(scanner) if scanner.check(/__\w+__\s*\(/) scanner.skip(/__\w+__/) skip_deadspace(scanner) collect_balanced(scanner, '(', ')') if scanner.peek(1) == '(' return true end if scanner.check(/__declspec\s*\(/) scanner.skip(/__declspec/) skip_deadspace(scanner) collect_balanced(scanner, '(', ')') if scanner.peek(1) == '(' return true end MSVC_CALLING_CONVENTIONS.each do |kw| if scanner.check(/#{Regexp.escape(kw)}\b/) scanner.skip(/#{Regexp.escape(kw)}/) return true end end false end |
#skip_deadspace(scanner) ⇒ Object
Skip "deadspace" - non-code elements that should be ignored during extraction Deadspace includes:
- Whitespace (spaces, tabs, newlines, carriage returns)
- Comments (both single-line // and multi-line /* */)
NOTE: Preprocessing directives (lines starting with #) are NOT deadspace — they are first-class features handled by CExtractorPreprocessing.
This method repeatedly scans for and skips these elements until no more are found, ensuring all consecutive deadspace is consumed in a single call.
Parameters:
scanner: StringScanner positioned at potential deadspace
Returns: Number of bytes skipped
Side effects: Advances scanner position past all consecutive deadspace
Examples:
" \n// comment\ncode" -> skips to "code"
"/* block */ \t\ncode" -> skips to "code"
"code" -> skips 0 bytes (no deadspace)
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/ceedling/c_extractor/c_extractor_code_text.rb', line 165 def skip_deadspace(scanner) start_pos = scanner.pos loop do initial = scanner.pos # Skip whitespace scanner.skip(/\s+/) # Skip comments skip_comment(scanner) if scanner.check(%r{/[/*]}) # If nothing was skipped, we're done break if scanner.pos == initial end return (scanner.pos - start_pos) end |
#skip_semicolons(scanner) ⇒ Object
Skip consecutive semicolons and any intervening deadspace
This method handles cases where multiple semicolons appear in sequence, potentially separated by whitespace or comments. This is valid C syntax (null statements) and can occur due to:
- Macro expansions
- Code generation
- Coding mistakes that don't cause compilation errors
The method repeatedly:
1. Skips any deadspace (whitespace and comments)
2. Checks for a semicolon
3. If found, consumes it and continues
4. If not found, restores position and exits
Parameters:
scanner: StringScanner positioned at potential semicolons/deadspace
Returns: Nothing (void method)
Side effects: Advances scanner position past all consecutive semicolons and deadspace
Examples:
";;;" -> skips all three semicolons
"; ; ;" -> skips semicolons and spaces
"; /* comment */ ;" -> skips semicolons and comment
"; code" -> skips first semicolon, stops at "code"
"code" -> skips nothing
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/ceedling/c_extractor/c_extractor_code_text.rb', line 126 def skip_semicolons(scanner) while !scanner.eos? start_pos = scanner.pos skip_deadspace(scanner) break if scanner.eos? # If we find a semicolon, consume it and continue if scanner.scan(/;/) next else # Not a semicolon, restore position and break scanner.pos = start_pos break end end end |
#strip_compiler_extensions(text) ⇒ Object
Strip all compiler extensions from a string and return the cleaned result. Uses an internal StringScanner plus collect_balanced() so all paren nesting depths are handled correctly (e.g. __declspec(align(8)), attribute((format(printf,1,2)))). Whitelist-based: only known forms are stripped; __int64, __int32, and any other non-extension __ identifiers are preserved verbatim. Whitespace is normalized to single spaces and the result is stripped.
Handles:
__word__(…) — any double-underscore attribute form
__declspec(…) — MSVC declaration specifier (including nested parens)
Bare MSVC calling conventions, MSVC inline hints, C11 specifier keywords
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/ceedling/c_extractor/c_extractor_code_text.rb', line 241 def strip_compiler_extensions(text) scanner = StringScanner.new(text) result = +"" = MSVC_CALLING_CONVENTIONS + ['__forceinline', '__inline__', '__inline'] + C11_SPECIFIER_KEYWORDS until scanner.eos? # __word__(…) — any double-underscore attribute form including __attribute__((…)) if scanner.check(/__\w+__\s*\(/) scanner.skip(/__\w+__/) skip_deadspace(scanner) collect_balanced(scanner, '(', ')') if scanner.peek(1) == '(' next end # __declspec(…) — handles nested forms like __declspec(align(8)) if scanner.check(/__declspec\s*\(/) scanner.skip(/__declspec/) skip_deadspace(scanner) collect_balanced(scanner, '(', ')') if scanner.peek(1) == '(' next end # Whitelisted bare keywords (calling conventions, inline hints, C11 specifiers) stripped = .any? do |kw| if scanner.check(/#{Regexp.escape(kw)}\b/) scanner.skip(/#{Regexp.escape(kw)}/) true end end next if stripped result << scanner.getch end result.gsub!(/\s+/, ' ') result.strip! result end |