blob: a09c24dea42f48f05b0a26eb3b66a2d7a0305fd5 [file] [log] [blame]
# 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