2023-08-10 09:59:44 +02:00
|
|
|
#!/usr/bin/env ruby
|
2023-08-02 09:21:56 +02:00
|
|
|
# Based on asciidoctor main ruby script.
|
|
|
|
# This is only meant to introspect and log which asciidoc files were used.
|
|
|
|
|
|
|
|
require 'asciidoctor'
|
|
|
|
require 'asciidoctor/cli'
|
|
|
|
|
2023-08-10 09:59:44 +02:00
|
|
|
MAIN_FILE_REGEX = /^.*\/rules\/(?<id>S\d+)\/(?:(?<lang>\w+)\/)?rule.adoc$/
|
|
|
|
|
2023-08-02 09:21:56 +02:00
|
|
|
class MainFileLogger < Asciidoctor::Extensions::Preprocessor
|
|
|
|
include Asciidoctor::Logging
|
|
|
|
|
|
|
|
def process document, reader
|
2023-08-10 09:59:44 +02:00
|
|
|
# Enable sourcemap to track source location.
|
|
|
|
# This is useful to report more accurate errors in other loggers.
|
|
|
|
document.sourcemap = true
|
|
|
|
|
2023-08-02 09:21:56 +02:00
|
|
|
main_file = document.normalize_system_path(reader.file, document.reader.dir)
|
|
|
|
|
|
|
|
# This assumes unix-style path separator.
|
2023-08-10 09:59:44 +02:00
|
|
|
if nil == main_file.match(MAIN_FILE_REGEX)
|
2023-08-02 09:21:56 +02:00
|
|
|
abort("Main file does not follow expected pattern: #{main_file}")
|
|
|
|
end
|
|
|
|
|
|
|
|
logger.info("ASCIIDOC LOGGER MAIN FILE:#{main_file}")
|
|
|
|
reader
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class IncludeLogger < Asciidoctor::Extensions::IncludeProcessor
|
|
|
|
include Asciidoctor::Logging
|
|
|
|
|
|
|
|
def initialize document
|
|
|
|
@config = {} # Defined in parent class; will be updated by the extension registry mechanism.
|
|
|
|
@document = document
|
|
|
|
|
|
|
|
# @document.reader.file is not defined yet at this stage.
|
|
|
|
# Therefore we cannot compute the main file path and cache it.
|
|
|
|
# This cannot be done once in handles? because the object is then frozen.
|
|
|
|
# For these reasons, we end up recomputing the rule directory path each time.
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_main_file reader
|
|
|
|
# See how include_stack is used:
|
|
|
|
# https://github.com/asciidoctor/asciidoctor/blob/f3800cc9c92faf8370041b2b27a61124318ed289/lib/asciidoctor/reader.rb#L669
|
|
|
|
if reader.include_stack.empty?
|
|
|
|
reader.file
|
|
|
|
else
|
|
|
|
main_frame = reader.include_stack.fetch(0)
|
|
|
|
main_frame.fetch(1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def handles? target
|
|
|
|
include_path = @document.normalize_system_path(target, @document.reader.dir)
|
|
|
|
|
|
|
|
main_file = get_main_file(@document.reader)
|
|
|
|
main_file = @document.normalize_system_path(main_file, @document.reader.dir)
|
|
|
|
rule_dir = File.dirname(File.dirname(main_file))
|
|
|
|
rule_id = File.basename(rule_dir)
|
|
|
|
if rule_id == 'rules'
|
|
|
|
# This is a language-agnostic rule description.
|
|
|
|
rule_dir = File.dirname(main_file)
|
|
|
|
rule_id = File.basename(rule_dir)
|
|
|
|
end
|
|
|
|
git_dir = File.dirname(File.dirname(rule_dir))
|
|
|
|
shared_dir = File.join(git_dir, 'shared_content')
|
|
|
|
|
|
|
|
rule_dir = rule_dir + '/' # Don't allow S100 to include things from S1000.
|
|
|
|
if !include_path.start_with?(rule_dir) && !include_path.start_with?(shared_dir)
|
2023-08-03 17:21:40 +02:00
|
|
|
logger.info("ASCIIDOC LOGGER CROSSREFERENCE:#{rule_id} cross-references #{include_path}")
|
2023-08-02 09:21:56 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
logger.info("ASCIIDOC LOGGER INCLUDE:#{include_path}")
|
|
|
|
|
|
|
|
false # Actually, this include processor does nothing.
|
|
|
|
end
|
|
|
|
|
|
|
|
# Intentionnaly no process function here.
|
|
|
|
end
|
|
|
|
|
2023-08-10 09:59:44 +02:00
|
|
|
class SourceLogger < Asciidoctor::Extensions::TreeProcessor
|
|
|
|
include Asciidoctor::Logging
|
|
|
|
|
|
|
|
def get_source_location block
|
|
|
|
loc = block.source_location # Asciidoctor::Reader::Cursor.
|
|
|
|
return "#{loc.file}:#{loc.lineno}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_rule document
|
|
|
|
main_file = document.normalize_system_path(document.reader.file, document.reader.dir)
|
|
|
|
res = main_file.match(MAIN_FILE_REGEX)
|
|
|
|
lang = res[:lang] || 'default'
|
|
|
|
"#{res[:id]}/#{lang}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def process document
|
|
|
|
rule = get_rule(document)
|
|
|
|
source_blocks = document.find_by(context: :listing, style: 'source')
|
|
|
|
|
|
|
|
# Collect individually valid blocks per diff-id.
|
|
|
|
blocks_per_id = Hash.new { |hash, key| hash[key] = Array.new }
|
|
|
|
|
|
|
|
# Find blocks with only diff-id but no diff-type, or vice-versa, or invalid diff-type.
|
|
|
|
source_blocks.each do |block|
|
|
|
|
id = block.attr('diff-id', nil)
|
|
|
|
type = block.attr('diff-type', nil)
|
|
|
|
|
|
|
|
if !id and !type
|
|
|
|
next # Nothing to validate
|
|
|
|
end
|
|
|
|
|
|
|
|
loc = get_source_location(block)
|
|
|
|
|
|
|
|
if !id
|
|
|
|
logger.info("ASCIIDOC LOGGER DIFF:[#{rule}] diff-id is missing in #{loc}")
|
|
|
|
next
|
|
|
|
end
|
|
|
|
|
|
|
|
if !type
|
|
|
|
logger.info("ASCIIDOC LOGGER DIFF:[#{rule}] diff-type is missing in #{loc}")
|
|
|
|
next
|
|
|
|
elsif !['compliant', 'noncompliant'].include?(type)
|
|
|
|
logger.info("ASCIIDOC LOGGER DIFF:[#{rule}] diff-type '#{type}' is not valid in #{loc}")
|
|
|
|
next
|
|
|
|
end
|
|
|
|
|
|
|
|
# The block is valid on its own.
|
|
|
|
blocks_per_id[id].push(block)
|
|
|
|
end
|
|
|
|
|
2023-09-12 13:16:52 +02:00
|
|
|
# Each diff-id should have:
|
|
|
|
# * exactly 1 noncompliant block, and
|
|
|
|
# * 1 or more compliant blocks.
|
2023-08-10 09:59:44 +02:00
|
|
|
# Find blocks that break this rule.
|
|
|
|
blocks_per_id.each do |id, blocks|
|
|
|
|
# Sort to ensure deterministic output.
|
|
|
|
blocks.sort_by! { |block| get_source_location(block) }
|
|
|
|
|
2023-09-12 13:16:52 +02:00
|
|
|
locs = blocks.map { |block| get_source_location(block) }.join(', ')
|
|
|
|
|
|
|
|
compliant = blocks.count { |block| block.attr('diff-type') == 'compliant' }
|
|
|
|
if compliant == 0
|
|
|
|
logger.info("ASCIIDOC LOGGER DIFF:[#{rule}] diff-id=#{id} has zero compliant example: #{locs}")
|
|
|
|
end
|
|
|
|
|
|
|
|
noncompliant = blocks.count { |block| block.attr('diff-type') == 'noncompliant' }
|
|
|
|
if noncompliant != 1
|
|
|
|
message = noncompliant == 0 ? "zero noncompliant example" : "too many noncompliant examples"
|
|
|
|
logger.info("ASCIIDOC LOGGER DIFF:[#{rule}] diff-id=#{id} has #{message}: #{locs}")
|
2023-08-10 09:59:44 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-08-02 09:21:56 +02:00
|
|
|
Asciidoctor::Extensions.register do
|
|
|
|
preprocessor MainFileLogger
|
|
|
|
include_processor IncludeLogger.new @document
|
2023-08-10 09:59:44 +02:00
|
|
|
treeprocessor SourceLogger
|
2023-08-02 09:21:56 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
invoker = Asciidoctor::Cli::Invoker.new ARGV
|
|
|
|
GC.start
|
|
|
|
invoker.invoke!
|
|
|
|
exit invoker.code
|