const CHARSET = 'a-zA-Z0-9%\\+\\-\\.@_\\*\\?\\/'; const ESCAPE_SET = 'abtnvfrE!"#\\$&\'\\(\\)\\*,;<>\\?\\[\\\\\\]^`{\\|}~' const NL = token.immediate(/[\r\n]+/); const WS = token.immediate(/[\t ]+/); const SPLIT = alias(token.immediate(seq('\\', /\r?\n|\r/)), '\\'); const AUTOMATIC_VARS = [ '@', '%', '<', '?', '^', '+', '/', '*' ]; const DEFINE_OPS = ['=', ':=', '::=', '?=', '+=']; const FUNCTIONS = [ 'subst', 'patsubst', 'strip', 'findstring', 'filter', 'filter-out', 'sort', 'word', 'words', 'wordlist', 'firstword', 'lastword', 'dir', 'notdir', 'suffix', 'basename', 'addsuffix', 'addprefix', 'join', 'wildcard', 'realpath', 'abspath', 'error', 'warning', 'info', 'origin', 'flavor', 'foreach', 'if', 'or', 'and', 'call', 'eval', 'file', 'value', ]; module.exports = grammar({ name: 'make', word: $ => $.word, inline: $ => [ $._targets, $._target_pattern, $._prerequisites_pattern, $._prerequisites, $._order_only_prerequisites, $._target_or_pattern_assignment, $._primary, $._name, $._string, ], extras: $ => [ /[\s]/, alias(token(seq('\\',/\r?\n|\r/)), '\\'), $.comment ], conflicts: $ => [], precedences: $ => [], rules: { // 3.1 makefile: $ => repeat($._thing), _thing: $ => choice( $.rule, $._variable_definition, $._directive, seq($._function, NL) ), // Rules {{{ // 2.1 rule: $ => choice( $._ordinary_rule, $._static_pattern_rule, ), _ordinary_rule: $ => prec.right(seq( $._targets, choice(':', '&:', '::'), optional(WS), optional($._prerequisites), choice( $.recipe, NL ) )), // 4.12.1 _static_pattern_rule: $ => prec.right(seq( $._targets, ':', optional(WS), $._target_pattern, ':', optional(WS), optional($._prerequisites_pattern), choice( $.recipe, NL ) )), _targets: $ => alias($.list, $.targets), // LINT: List shall have length one _target_pattern: $ => field( 'target', alias($.list, $.pattern_list) ), // 4.3 _prerequisites: $ => choice( $._normal_prerequisites, seq( optional($._normal_prerequisites), '|', $._order_only_prerequisites ), ), _normal_prerequisites: $ => field( 'normal', alias($.list, $.prerequisites), ), _order_only_prerequisites: $ => field( 'order_only', alias($.list, $.prerequisites) ), _prerequisites_pattern: $ => field( 'prerequisite', alias($.list, $.pattern_list) ), recipe: $ => prec.right(choice( // the first recipe line may be attached to the // target-and-prerequisites line with a semicolon // in between seq( $._attached_recipe_line, NL, repeat(choice( $.conditional, $._prefixed_recipe_line, )) ), seq( NL, repeat1(choice( $.conditional, $._prefixed_recipe_line )) ), )), _attached_recipe_line: $ => seq( ';', optional($.recipe_line) ), _prefixed_recipe_line: $ => seq( $._recipeprefix, optional($.recipe_line), NL ), recipe_line: $ => seq( optional(choice( ...['@', '-', '+'].map(c => token(prec(1,c))) )), optional(seq( alias($.shell_text_with_split, $.shell_text), repeat(seq( // splited recipe lines may start with .RECIPEPREFIX // that shall not be part of the shell_code optional($._recipeprefix), alias($.shell_text_with_split, $.shell_text), )), optional($._recipeprefix), )), alias($._shell_text_without_split, $.shell_text), ), // }}} // Variables {{{ _variable_definition: $ => choice( $.VPATH_assignment, $.RECIPEPREFIX_assignment, $.variable_assignment, $.shell_assignment, $.define_directive ), // 4.5.1 VPATH_assignment: $ => seq( field('name','VPATH'), optional(WS), field('operator',choice(...DEFINE_OPS)), field('value',$.paths), NL ), RECIPEPREFIX_assignment: $ => seq( field('name','.RECIPEPREFIX'), optional(WS), field('operator',choice(...DEFINE_OPS)), field('value', $.text), NL ), // 6.5 variable_assignment: $ => seq( optional($._target_or_pattern_assignment), $._name, optional(WS), field('operator',choice(...DEFINE_OPS)), optional(WS), optional(field('value', $.text)), NL ), _target_or_pattern_assignment: $ => seq( field('target_or_pattern', $.list), ':', optional(WS), ), shell_assignment: $ => seq( field('name',$.word), optional(WS), field('operator','!='), optional(WS), field('value',$._shell_command), NL ), define_directive: $ => seq( 'define', field('name',$.word), optional(WS), optional(field('operator',choice(...DEFINE_OPS))), optional(WS), NL, optional(field('value', alias(repeat1($._rawline), $.raw_text) )), token(prec(1,'endef')), NL ), // }}} // Directives {{{ _directive: $ => choice( $.include_directive, $.vpath_directive, $.export_directive, $.unexport_directive, $.override_directive, $.undefine_directive, $.private_directive, $.conditional ), // 3.3 include_directive: $ => choice( seq( 'include', field('filenames',$.list), NL), seq('sinclude', field('filenames',$.list), NL), seq('-include', field('filenames',$.list), NL), ), // 4.5.2 vpath_directive: $ => choice( seq('vpath', NL), seq('vpath', field('pattern', $.word), NL), seq('vpath', field('pattern', $.word), field('directories', $.paths), NL) ), // 5.7.2 export_directive: $ => choice( seq('export', NL), seq('export', field('variables', $.list), NL), seq('export', $.variable_assignment) ), // 5.7.2 unexport_directive: $ => choice( seq('unexport', NL), seq('unexport', field('variables', $.list), NL), ), // 6.7 override_directive: $ => choice( seq('override', $.define_directive), seq('override', $.variable_assignment), seq('override', $.undefine_directive), ), // 6.9 undefine_directive: $ => seq( 'undefine', field('variable', $.word), NL ), // 6.13 private_directive: $ => seq( 'private', $.variable_assignment ), // }}} // Conditionals {{{ // 7 conditional: $ => seq( field('condition', $._conditional_directives), optional(field('consequence', $._conditional_consequence)), repeat($.elsif_directive), optional($.else_directive), 'endif', NL ), elsif_directive: $ => seq( 'else', field('condition', $._conditional_directives), optional(field('consequence', $._conditional_consequence)), ), else_directive: $ => seq( 'else', NL, optional(field('consequence', $._conditional_consequence)), ), _conditional_directives: $ => choice( $.ifeq_directive, $.ifneq_directive, $.ifdef_directive, $.ifndef_directive ), _conditional_consequence: $ => repeat1(choice( $._thing, $._prefixed_recipe_line )), ifeq_directive: $ => seq( 'ifeq', $._conditional_args_cmp, NL ), ifneq_directive: $ => seq( 'ifneq', $._conditional_args_cmp, NL ), ifdef_directive: $ => seq( 'ifdef', field('variable', $._primary), NL ), ifndef_directive: $ => seq( 'ifndef', field('variable', $._primary), NL ), _conditional_args_cmp: $ => choice( seq( '(', optional(field('arg0', $._primary)), ',', optional(field('arg1', $._primary)), ')' ), seq( field('arg0', $._primary), field('arg1', $._primary), ), ), // }}} // Variables {{{ _variable: $ => choice( $.variable_reference, $.substitution_reference, $.automatic_variable, ), variable_reference: $ => seq( choice('$','$$'), choice( delimitedVariable($._primary), // TODO are those legal? $) $$$ alias(token.immediate(/./), $.word), // match any single digit //alias(token.immediate('\\\n'), $.word) ) ), // 6.3.1 substitution_reference: $ => seq( choice('$','$$'), delimitedVariable(seq( field('text',$._primary), ':', field('pattern',$._primary), '=', field('replacement',$._primary), )), ), // 10.5.3 automatic_variable: $ => seq( choice('$','$$'), choice( choice( ...AUTOMATIC_VARS .map(c => token.immediate(prec(1,c))) ), delimitedVariable(seq( choice( ...AUTOMATIC_VARS .map(c => token(prec(1,c))) ), optional(choice( token.immediate('D'), token.immediate('F') )), )) ) ), // }}} // Functions {{{ _function: $ => choice( $.function_call, $.shell_function, ), function_call: $ => seq( choice('$','$$'), token.immediate('('), field('function', choice( ...FUNCTIONS.map(f => token.immediate(f)) )), optional(WS), $.arguments, ')' ), arguments: $ => seq( field('argument',$.text), repeat(seq( ',', field('argument',$.text), )) ), // 8.13 shell_function: $ => seq( choice('$','$$'), token.immediate('('), field('function', 'shell'), optional(WS), $._shell_command, ')' ), // }}} // Primary and lists {{{ list: $ => prec(1,seq( $._primary, repeat(seq( choice(WS, SPLIT), $._primary )), optional(WS) )), paths: $ => seq( $._primary, repeat(seq( choice(...[':',';'].map(c=>token.immediate(c))), $._primary )), ), _primary: $ => choice( $.word, $.archive, $._variable, $._function, $.concatenation, $.string, ), concatenation: $ => prec.right(seq( $._primary, repeat1(prec.left($._primary)) )), // }}} // Names {{{ _name: $ => field('name',$.word), string: $ => field('string',choice( seq('"', optional($._string), '"'), seq("'", optional($._string), "'"), )), _string: $ => repeat1(choice( $._variable, $._function, token(prec(-1,/([^'"$\r\n\\]|\\\\|\\[^\r\n])+/)), )), word: $ => token(repeat1(choice( new RegExp ('['+CHARSET+']'), new RegExp ('\\\\['+ESCAPE_SET+']'), new RegExp ('\\\\[0-9]{3}'), ))), // 11.1 archive: $ => seq( field('archive', $.word), token.immediate('('), field('members', $.list), token.immediate(')'), ), // }}} // Tokens {{{ // TODO external parser for .RECIPEPREFIX _recipeprefix: $ => '\t', // TODO prefixed line in define is recipe _rawline: $ => token(/.*[\r\n]+/), // any line _shell_text_without_split: $ => text($, noneOf(...['\\$', '\\r', '\\n', '\\']), choice( $._variable, $._function, alias('$$',$.escape), alias('//',$.escape), ), ), // The SPLIT chars shall be included the injected code shell_text_with_split: $ => seq( $._shell_text_without_split, SPLIT, ), _shell_command: $ => alias( $.text, $.shell_command ), text: $ => text($, choice( noneOf(...['\\$', '\\(', '\\)', '\\n', '\\r', '\\']), SPLIT ), choice( $._variable, $._function, alias('$$',$.escape), alias('//',$.escape), ), ), // }}} comment: $ => token(prec(-1,/#.*/)), } }); function noneOf(...characters) { const negatedString = characters.map(c => c == '\\' ? '\\\\' : c).join('') return new RegExp('[^' + negatedString + ']') } function delimitedVariable(rule) { return choice( seq(token.immediate('('), rule, ')'), seq(token.immediate('{'), rule, '}') ) } function text($, text, fenced_vars) { const raw_text = token(repeat1(choice( text, new RegExp ('\\\\['+ESCAPE_SET+']'), new RegExp ('\\\\[0-9]{3}'), new RegExp ('\\\\[^\n\r]'), // used in cmd like sed \1 ))) return choice( seq( raw_text, repeat(seq( fenced_vars, optional(raw_text) )), ), seq( fenced_vars, repeat(seq( optional(raw_text), fenced_vars )), optional(raw_text) ) ) }