# Copyright 2016-2023 The Khronos Group Inc. # # SPDX-License-Identifier: Apache-2.0 require 'asciidoctor/extensions' unless RUBY_ENGINE == 'opal' include ::Asciidoctor module Asciidoctor class IfDefMismatchPreprocessorReader < PreprocessorReader attr_reader :found_conditionals attr_reader :warned class CursorWithAttributes attr_reader :cursor attr_reader :attributes attr_reader :line def initialize cursor, attributes, line @cursor, @attributes, @line = cursor, attributes, line end end def initialize document, lines @found_conditionals = Array.new super(document,lines) end def is_adoc_begin_conditional line line.start_with?( 'ifdef::', 'ifndef::' ) && line.end_with?('[]') end def is_adoc_begin_conditional_eval line line.start_with?( 'ifeval::[' ) && line.end_with?(']') end def is_adoc_end_conditional line line.start_with?( 'endif::' ) && line.end_with?('[]') end def conditional_attributes line line.delete_prefix('ifdef::').delete_prefix('ifndef::').delete_prefix('endif::').delete_suffix('[]') end def process_line line new_line = line if is_adoc_begin_conditional(line) # Standard conditionals, add the conditional to a stack to be unwound as endifs are found @found_conditionals.push CursorWithAttributes.new cursor, conditional_attributes(line), line elsif is_adoc_begin_conditional_eval(line) # ifeval conditionals do not have attributes, so store those slightly differently @found_conditionals.push CursorWithAttributes.new cursor, '', line elsif is_adoc_end_conditional(line) # Try to match each endif to a previously defined conditional, logging errors as it goes match_found = false pop_count = 0 error_stack = Array.new @found_conditionals.reverse_each do |conditional| # Try the whole stack to find a match in case there is an extra ifdef in the way pop_count += 1 if conditional.attributes == conditional_attributes(line) match_found = true break end end if match_found # First pop any non-matching conditionals and fire a mismatch error (pop_count - 1).times do # Warn about fixing preprocessor directives before any other issue, as these often cause a domino effect if not @warned logger.warn "Preprocessor conditional mismatch detected - these should be addressed before attempting to fix any other errors." @warned = true end # Log an error conditional = @found_conditionals.pop logger.error message_with_context %(unmatched conditional "#{conditional.line}" with no endif), source_location: conditional.cursor # Insert an endif statement so asciidoctor's default reader does not throw extraneous mismatch errors. # This can mess with the way blocks are terminated, but errors will only be thrown if they would have been thrown without this checker; they will just be different. # # e.g.: # [source,c] # ---- # ifdef::undefined_attribute[] # Some text # ifdef::undefined_attribute[] // should be an endif # ---- // left unparsed because the ifdef is open # // Script adds 2 'endif::undefined_attribute[]' lines here # endif::another_attribute[] // Irrelevant whether this is defined or not # # Ideally these errors would be suppressed too, but that requires a lot more complexity; e.g. rewinding the reader back to the ifdef and removing it extra_line = %(endif::#{conditional.attributes}[]) unshift(extra_line) super(extra_line) end # Pop the matching conditional @found_conditionals.pop else # Warn about fixing preprocessor directives before any other issue, as these often cause a domino effect if not @warned logger.warn "Preprocessor conditional mismatch detected - these should be addressed before attempting to fix any other errors." @warned = true end # If no match was found, then this is an orphaned endif logger.error message_with_context %(unmatched endif - found "#{line}" with no matching conditional begin), source_location: cursor # Hide the endif so that asciidoctor's default reader does not try to match it anyway new_line = '' end end super(new_line) end end # Preprocessor hook to iterate over ifdefs to prevent them from affecting asciidoctor's processing. class IfDefMismatchPreprocessor < Extensions::Preprocessor def process document, reader # Create a new reader to return which raises errors for mismatched conditionals reader = IfDefMismatchPreprocessorReader.new(document, reader.lines) end end end