github.com/aretext/aretext@v1.3.0/syntax/languages/makefile.go (about) 1 package languages 2 3 import ( 4 "io" 5 6 "github.com/aretext/aretext/syntax/parser" 7 ) 8 9 const ( 10 // This is for variable and function expansions. 11 // Examples: 12 // $(VAR) 13 // ${VAR} 14 // $(func arg1 arg2) 15 makefileTokenRoleVariable = parser.TokenRoleCustom1 16 17 // This is for special patterns in targets or prereqs. 18 // Example: 19 // %.o: %.c 20 makefileTokenRolePattern = parser.TokenRoleCustom2 21 ) 22 23 type makefileParseState uint8 24 25 const ( 26 // Top-level is the initial state. 27 makefileTopLevelParseState = makefileParseState(iota) 28 29 // Rule prereq when we're in the "prerequisites" section of a rule. 30 // 31 // Example: 32 // foo: bar baz # "bar" and "baz" are the prereqs. 33 makefileRulePrereqParseState 34 35 // Recipe is the section of a rule with shell commands. 36 // 37 // Example: 38 // foo: bar baz 39 // echo "hello" # <-- this is the recipe. 40 makefileRecipeCmdParseState 41 42 // AssignmentVal is the value assigned to a variable. 43 // 44 // Example: 45 // X = abc <-- "abc" is the assignment val state. 46 makefileAssignmentValParseState 47 ) 48 49 func (s makefileParseState) Equals(other parser.State) bool { 50 otherState, ok := other.(makefileParseState) 51 return ok && s == otherState 52 } 53 54 // MakefileParseFunc returns a parse func for GNU Makefiles. 55 // See https://www.gnu.org/software/make/manual/make.html 56 // especially section 3.8 "How Makefiles are Parsed" 57 func MakefileParseFunc() parser.Func { 58 // From top-level, if we see ":" then we must be in a rule, 59 // so start parsing the prereqs and then parse the following tab-indented 60 // lines as recipe commands. 61 parseRuleSeparator := matchState( 62 makefileTopLevelParseState, 63 consumeString(":"). 64 ThenNot(consumeString("=")). 65 Map(setState(makefileRulePrereqParseState))) 66 67 // Recipe command are tab-indented, or following rule prereqs separated by a semicolon. 68 // If the start of the recipe command has "@", treat that as an operator 69 // meaning "do not echo this line". 70 // 71 // Treating every tab-indented line as part of a recipe isn't completely accurate, 72 // since technically it's missing the target/prereqs, but I believe other editors 73 // also make this assumption. 74 parseAtOp := consumeString("@"). 75 Map(recognizeToken(parser.TokenRoleOperator)) 76 77 parseTabIndent := consumeString("\n\t"). 78 ThenMaybe(parseAtOp). 79 Map(setState(makefileRecipeCmdParseState)) 80 81 parseSemicolonInRule := matchState( 82 makefileRulePrereqParseState, 83 consumeString(";"). 84 ThenMaybe(consumeRunesLike(func(r rune) bool { return r == ' ' || r == '\t' })). 85 ThenMaybe(parseAtOp). 86 Map(setState(makefileRecipeCmdParseState))) 87 88 // Handle backslash line continuation everywhere except in recipe command. 89 // This consumes the backslash and newline, then stays in the same state 90 // rather than transitioning back to top-level. 91 parseBackslashLineContinuation := matchStates( 92 []parser.State{makefileRulePrereqParseState, makefileRecipeCmdParseState, makefileAssignmentValParseState}, 93 consumeString(`\`). 94 ThenMaybe(consumeRunesLike(func(r rune) bool { return r == ' ' || r == '\t' })). 95 Then(consumeString("\n"))) 96 97 // A newline NOT followed by a tab transitions back to top-level state. 98 parseBackToTopLevel := matchStates( 99 []parser.State{makefileRulePrereqParseState, makefileRecipeCmdParseState, makefileAssignmentValParseState}, 100 consumeString("\n"). 101 Map(setState(makefileTopLevelParseState))) 102 103 // Parse some keywords only at top-level. 104 parseTopLevelKeywords := matchState( 105 makefileTopLevelParseState, 106 consumeLongestMatchingOption([]string{ 107 "ifeq", "ifneq", "ifdef", "ifndef", "else", "endif", 108 "export", "unexport", "override", 109 "include", "-include", "define", "endef", 110 }).Map(recognizeToken(parser.TokenRoleKeyword))) 111 112 // Parse comments everywhere except in recipe commands. 113 parseComment := matchStates( 114 []parser.State{makefileTopLevelParseState, makefileRulePrereqParseState}, 115 makefileCommentParseFunc()) 116 117 // Parse assign operators everywhere except in recipe commands. 118 parseAssignOp := matchStates( 119 []parser.State{makefileTopLevelParseState, makefileRulePrereqParseState}, 120 consumeLongestMatchingOption([]string{"=", ":=", "::=", ":::=", "?=", "+=", "!="}). 121 Map(recognizeToken(parser.TokenRoleOperator)). 122 Map(setState(makefileAssignmentValParseState))) 123 124 // Parse escaped dollar sign ($$). 125 // This must come before any rule that parses "$" as a prefix 126 // to ensure that "$$" isn't highlighted. 127 parseEscapedDollarSign := consumeString("$$") 128 129 // Parse automatic variables in all states. 130 parseAutomaticVariables := consumeLongestMatchingOption([]string{ 131 "$@", "$%", "$<", "$?", "$^", "$+", "$|", "$*", 132 }).Map(recognizeToken(makefileTokenRoleVariable)) 133 134 // Parse patterns only in rule target and prereqs (NOT in assignment). 135 parseRulePattern := matchStates( 136 []parser.State{makefileTopLevelParseState, makefileRulePrereqParseState}, 137 consumeLongestMatchingOption([]string{ 138 "%", "%D", "%F", "+", "+D", "+F", 139 }).Map(recognizeToken(makefileTokenRolePattern))) 140 141 // Parse expansions (functions and variables) in all states. 142 parseExpansion := consumeString("$"). 143 Then(makefileExpansionParseFunc()). 144 Map(recognizeToken(makefileTokenRoleVariable)) 145 146 return initialState( 147 makefileTopLevelParseState, 148 parseRuleSeparator. 149 Or(parseTabIndent). 150 Or(parseSemicolonInRule). 151 Or(parseBackslashLineContinuation). 152 Or(parseBackToTopLevel). 153 Or(parseComment). 154 Or(parseAssignOp). 155 Or(parseEscapedDollarSign). 156 Or(parseAutomaticVariables). 157 Or(parseRulePattern). 158 Or(parseExpansion). 159 Or(parseTopLevelKeywords)) 160 } 161 162 // makefileCommentParseFunc parses a comment in a makefile. 163 // Comments are just "# ..." to end of line, but do NOT consume 164 // the line feed, since that determines state transitions. 165 func makefileCommentParseFunc() parser.Func { 166 return func(iter parser.TrackingRuneIter, state parser.State) parser.Result { 167 var numConsumed uint64 168 169 startRune, err := iter.NextRune() 170 if err != nil || startRune != '#' { 171 return parser.FailedResult 172 } 173 numConsumed++ 174 175 for { 176 r, err := iter.NextRune() 177 if err == io.EOF { 178 break 179 } else if err != nil { 180 return parser.FailedResult 181 } 182 183 if r == '\n' { 184 // Do NOT consume the line feed. 185 break 186 } 187 188 numConsumed++ 189 } 190 191 return parser.Result{ 192 NumConsumed: numConsumed, 193 ComputedTokens: []parser.ComputedToken{ 194 { 195 Length: numConsumed, 196 Role: parser.TokenRoleComment, 197 }, 198 }, 199 NextState: state, 200 } 201 } 202 } 203 204 // makefileExpansionParseFunc handles variable and function expansions. 205 // 206 // Examples: 207 // 208 // ${VAR} 209 // $(VAR) 210 // $(subst $(space),$(comma),$(foo)) 211 func makefileExpansionParseFunc() parser.Func { 212 return func(iter parser.TrackingRuneIter, state parser.State) parser.Result { 213 var n uint64 214 215 // Open delimiter. 216 startRune, err := iter.NextRune() 217 if err != nil || !(startRune == '{' || startRune == '(') { 218 return parser.FailedResult 219 } 220 n++ 221 222 // Maintain a stack of open delimiters so we can check when they're closed. 223 stack := []rune{startRune} 224 225 // Consume runes until the stack is empty. 226 for len(stack) > 0 { 227 stackTop := stack[len(stack)-1] 228 229 r, err := iter.NextRune() 230 if err != nil { 231 return parser.FailedResult 232 } 233 n++ 234 235 if r == '{' || r == '(' { 236 // Push open delimiter to stack. 237 stack = append(stack, r) 238 } else if (stackTop == '{' && r == '}') || (stackTop == '(' && r == ')') { 239 // Found close delimiter matching last open delimiter, so pop from stack. 240 stack = stack[0 : len(stack)-1] 241 } 242 } 243 244 return parser.Result{ 245 NumConsumed: n, 246 ComputedTokens: []parser.ComputedToken{ 247 {Length: n}, 248 }, 249 NextState: state, 250 } 251 } 252 }