-- The preprocessing step of static analysis determines which parts of the input files contain expl3 code. local lpeg = require("lpeg") local Cp, P, R, S, V = lpeg.Cp, lpeg.P, lpeg.R, lpeg.S, lpeg.V -- Define base parsers. ---- Generic local any = P(1) local eof = -any ---- Tokens local lbrace = P("{") local rbrace = P("}") local percent_sign = P("%") local backslash = P([[\]]) local letter = R("AZ","az") local underscore = P("_") local colon = P(":") ---- Spacing local newline = ( P("\n") + P("\r\n") + P("\r") ) local linechar = any - newline local spacechar = S("\t ") local optional_spaces = spacechar^0 local optional_spaces_and_newline = ( optional_spaces * ( newline * optional_spaces )^-1 ) -- Define intermediate parsers. ---- Parts of TeX syntax local comment = ( percent_sign * linechar^0 * newline * optional_spaces ) local argument = ( lbrace * ( comment + (any - rbrace) )^0 * rbrace ) local expl3like_control_sequence = ( backslash * (letter - underscore - colon)^1 * (underscore + colon) * letter^1 ) ---- Standard delimiters local provides = ( P([[\ProvidesExpl]]) * ( P("Package") + P("Class") + P("File") ) * optional_spaces_and_newline * comment^0 * argument * optional_spaces_and_newline * comment^0 * argument * optional_spaces_and_newline * comment^0 * argument * optional_spaces_and_newline * comment^0 * argument ) local expl_syntax_on = P([[\ExplSyntaxOn]]) local expl_syntax_off = P([[\ExplSyntaxOff]]) local function preprocessing(issues, content, max_line_length) if max_line_length == nil then max_line_length = 80 end -- Determine the bytes where lines begin. local line_starting_byte_numbers = {} local function record_line(line_start) table.insert(line_starting_byte_numbers, line_start) end local function line_too_long(range_start, range_end) issues:add('s103', 'line too long', range_start, range_end + 1) end local line_numbers_grammar = ( Cp() / record_line * ( ( ( Cp() * linechar^(max_line_length + 1) * Cp() / line_too_long + linechar^0 ) * newline * Cp() ) / record_line )^0 ) lpeg.match(line_numbers_grammar, content) -- Determine which parts of the input files contain expl3 code. local expl_ranges = {} local function capture_range(range_start, range_end) table.insert(expl_ranges, {range_start, range_end + 1}) end local function unexpected_pattern(pattern, code, message, test) return Cp() * pattern * Cp() / function(range_start, range_end) if test == nil or test() then issues:add(code, message, range_start, range_end + 1) end end end local num_provides = 0 local analysis_grammar = P{ "Root"; Root = ( ( V"NonExplPart" * V"ExplPart" / capture_range )^0 * V"NonExplPart" ), NonExplPart = ( ( unexpected_pattern( V"Closer", "w101", "unexpected delimiters" ) + unexpected_pattern( expl3like_control_sequence, "e102", "expl3 control sequences in non-expl3 parts" ) + (any - V"Opener") )^0 ), ExplPart = ( V"Opener" * Cp() * ( unexpected_pattern( V"Opener", "w101", "unexpected delimiters" ) + (any - V"Closer") )^0 * Cp() * (V"Closer" + eof) ), Opener = ( expl_syntax_on + unexpected_pattern( provides, "e104", [[multiple delimiters `\ProvidesExpl*` in a single file]], function() num_provides = num_provides + 1 return num_provides > 1 end ) ), Closer = expl_syntax_off, } lpeg.match(analysis_grammar, content) -- If no parts were detected, assume that the whole input file is in expl3. if(#expl_ranges == 0 and #content > 0) then table.insert(expl_ranges, {0, #content}) issues:add('w100', 'no standard delimiters') issues:ignore('e102') end return line_starting_byte_numbers, expl_ranges end return preprocessing