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,
],
rules: {
makefile: $ => repeat($._thing),
_thing: $ => choice(
$.rule,
$._variable_definition,
$._directive,
seq($._function, NL),
),
rule: $ => choice(
$._ordinary_rule,
$._static_pattern_rule,
),
_ordinary_rule: $ => prec.right(seq(
$._targets,
choice(':', '&:', '::'),
optional(WS),
optional($._prerequisites),
choice(
$.recipe,
NL,
),
)),
_static_pattern_rule: $ => prec.right(seq(
$._targets,
':',
optional(WS),
$._target_pattern,
':',
optional(WS),
optional($._prerequisites_pattern),
choice(
$.recipe,
NL,
),
)),
_targets: $ => alias($.list, $.targets),
_target_pattern: $ => field(
'target',
alias($.list, $.pattern_list),
),
_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(
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(
optional($._recipeprefix),
alias($.shell_text_with_split, $.shell_text),
)),
optional($._recipeprefix),
)),
alias($._shell_text_without_split, $.shell_text),
),
_variable_definition: $ => choice(
$.VPATH_assignment,
$.RECIPEPREFIX_assignment,
$.variable_assignment,
$.shell_assignment,
$.define_directive,
),
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,
),
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,
),
_directive: $ => choice(
$.include_directive,
$.vpath_directive,
$.export_directive,
$.unexport_directive,
$.override_directive,
$.undefine_directive,
$.private_directive,
$.conditional,
),
include_directive: $ => choice(
seq('include', field('filenames', $.list), NL),
seq('sinclude', field('filenames', $.list), NL),
seq('-include', field('filenames', $.list), NL),
),
vpath_directive: $ => choice(
seq('vpath', NL),
seq('vpath', field('pattern', $.word), NL),
seq('vpath', field('pattern', $.word), field('directories', $.paths), NL),
),
export_directive: $ => choice(
seq('export', NL),
seq('export', field('variables', $.list), NL),
seq('export', $.variable_assignment),
),
unexport_directive: $ => choice(
seq('unexport', NL),
seq('unexport', field('variables', $.list), NL),
),
override_directive: $ => choice(
seq('override', $.define_directive),
seq('override', $.variable_assignment),
seq('override', $.undefine_directive),
),
undefine_directive: $ => seq(
'undefine', field('variable', $.word), NL,
),
private_directive: $ => seq(
'private', $.variable_assignment,
),
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),
),
),
_variable: $ => choice(
$.variable_reference,
$.substitution_reference,
$.automatic_variable,
),
variable_reference: $ => seq(
choice('$', '$$'),
choice(
delimitedVariable($._primary),
alias(token.immediate(/./), $.word), ),
),
substitution_reference: $ => seq(
choice('$', '$$'),
delimitedVariable(seq(
field('text', $._primary),
':',
field('pattern', $._primary),
'=',
field('replacement', $._primary),
)),
),
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'),
)),
)),
),
),
_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),
)),
),
shell_function: $ => seq(
choice('$', '$$'),
token.immediate('('),
field('function', 'shell'),
optional(WS),
$._shell_command,
')',
),
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)),
)),
_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}'),
))),
archive: $ => seq(
field('archive', $.word),
token.immediate('('),
field('members', $.list),
token.immediate(')'),
),
_recipeprefix: _ => '\t',
_rawline: _ => token(/.*[\r\n]+/),
_shell_text_without_split: $ => text(
noneOf(...['\\$', '\\r', '\\n', '\\']),
choice(
$._variable,
$._function,
alias('$$', $.escape),
alias('//', $.escape),
),
),
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]'), )));
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),
),
);
}