feat: add Erlang problem matchers (#433)
cfafb69 · Daniel Widgren · 2026-03-14 16:43
Message
{commit_body(@commit)}
Files changed
added
dist/erlang-matchers.json
+66
−0
@@ -0,0 +1,66 @@
| 1 | +{ | |
| 2 | + "problemMatcher": [ | |
| 3 | + { | |
| 4 | + "owner": "erlc-warning", | |
| 5 | + "severity": "warning", | |
| 6 | + "pattern": [ | |
| 7 | + { | |
| 8 | + "regexp": "^(\\S+):(\\d+):(\\d+): [wW]arning: (.+)$", | |
| 9 | + "file": 1, | |
| 10 | + "line": 2, | |
| 11 | + "column": 3, | |
| 12 | + "message": 4 | |
| 13 | + } | |
| 14 | + ] | |
| 15 | + }, | |
| 16 | + { | |
| 17 | + "owner": "erlc-error", | |
| 18 | + "severity": "error", | |
| 19 | + "pattern": [ | |
| 20 | + { | |
| 21 | + "regexp": "^(\\S+):(\\d+):(\\d+): (?![wW]arning)(.+)$", | |
| 22 | + "file": 1, | |
| 23 | + "line": 2, | |
| 24 | + "column": 3, | |
| 25 | + "message": 4 | |
| 26 | + } | |
| 27 | + ] | |
| 28 | + }, | |
| 29 | + { | |
| 30 | + "owner": "ct-failure", | |
| 31 | + "severity": "error", | |
| 32 | + "pattern": [ | |
| 33 | + { | |
| 34 | + "regexp": "^(\\S+) failed on line (\\d+)$", | |
| 35 | + "file": 1, | |
| 36 | + "line": 2 | |
| 37 | + }, | |
| 38 | + { | |
| 39 | + "regexp": "^Reason: (.+)$", | |
| 40 | + "message": 1 | |
| 41 | + } | |
| 42 | + ] | |
| 43 | + }, | |
| 44 | + { | |
| 45 | + "owner": "eunit-failure", | |
| 46 | + "severity": "error", | |
| 47 | + "pattern": [ | |
| 48 | + { | |
| 49 | + "regexp": "^\\S+: .+\\.\\.\\.\\*failed\\*$" | |
| 50 | + }, | |
| 51 | + { | |
| 52 | + "regexp": "^in function .+ \\((.+), line (\\d+)\\)$", | |
| 53 | + "file": 1, | |
| 54 | + "line": 2 | |
| 55 | + }, | |
| 56 | + { | |
| 57 | + "regexp": "^in call from .+$" | |
| 58 | + }, | |
| 59 | + { | |
| 60 | + "regexp": "^\\*\\*error:(.+)$", | |
| 61 | + "message": 1 | |
| 62 | + } | |
| 63 | + ] | |
| 64 | + } | |
| 65 | + ] | |
| 66 | +} |
modified
dist/index.js
+14
−9
@@ -57588,6 +57588,7 @@ async function installOTP(otpSpec) {
| 57588 | 57588 | }, |
| 57589 | 57589 | }) |
| 57590 | 57590 | setOutput('otp-version', otpVersion) |
| 57591 | + maybeEnableProblemMatchers('erlang') | |
| 57591 | 57592 | endGroup() |
| 57592 | 57593 | |
| 57593 | 57594 | return otpVersion |
@@ -57609,7 +57610,7 @@ async function maybeInstallElixir(elixirSpec) {
| 57609 | 57610 | }, |
| 57610 | 57611 | }) |
| 57611 | 57612 | setOutput('elixir-version', elixirVersion) |
| 57612 | − maybeEnableElixirProblemMatchers() | |
| 57613 | + maybeEnableProblemMatchers('elixir') | |
| 57613 | 57614 | endGroup() |
| 57614 | 57615 | |
| 57615 | 57616 | installed = true |
@@ -57618,14 +57619,18 @@ async function maybeInstallElixir(elixirSpec) {
| 57618 | 57619 | return installed |
| 57619 | 57620 | } |
| 57620 | 57621 | |
| 57621 | −function maybeEnableElixirProblemMatchers() { | |
| 57622 | − const disableProblemMatchers = setup_beam_getInput('disable_problem_matchers', false) | |
| 57623 | − if (disableProblemMatchers === 'false') { | |
| 57624 | − // path.join helps ncc figure this out | |
| 57625 | − const elixirMatchers = external_node_path_namespaceObject.join( | |
| 57626 | − __nccwpck_require__.ab + "elixir-matchers.json", | |
| 57627 | − ) | |
| 57628 | − info(`##[add-matcher]${elixirMatchers}`) | |
| 57622 | +function problemMatchersEnabled() { | |
| 57623 | + return setup_beam_getInput('disable_problem_matchers', false) === 'false' | |
| 57624 | +} | |
| 57625 | + | |
| 57626 | +// path.join with static strings helps ncc resolve and bundle the JSON files | |
| 57627 | +function maybeEnableProblemMatchers(language) { | |
| 57628 | + if (problemMatchersEnabled()) { | |
| 57629 | + const matcherFiles = { | |
| 57630 | + erlang: external_node_path_namespaceObject.join(__nccwpck_require__.ab + "erlang-matchers.json"), | |
| 57631 | + elixir: external_node_path_namespaceObject.join(__nccwpck_require__.ab + "elixir-matchers.json"), | |
| 57632 | + } | |
| 57633 | + info(`##[add-matcher]${matcherFiles[language]}`) | |
| 57629 | 57634 | } |
| 57630 | 57635 | } |
| 57631 | 57636 |
added
matchers/erlang-matchers.json
+66
−0
@@ -0,0 +1,66 @@
| 1 | +{ | |
| 2 | + "problemMatcher": [ | |
| 3 | + { | |
| 4 | + "owner": "erlc-warning", | |
| 5 | + "severity": "warning", | |
| 6 | + "pattern": [ | |
| 7 | + { | |
| 8 | + "regexp": "^(\\S+):(\\d+):(\\d+): [wW]arning: (.+)$", | |
| 9 | + "file": 1, | |
| 10 | + "line": 2, | |
| 11 | + "column": 3, | |
| 12 | + "message": 4 | |
| 13 | + } | |
| 14 | + ] | |
| 15 | + }, | |
| 16 | + { | |
| 17 | + "owner": "erlc-error", | |
| 18 | + "severity": "error", | |
| 19 | + "pattern": [ | |
| 20 | + { | |
| 21 | + "regexp": "^(\\S+):(\\d+):(\\d+): (?![wW]arning)(.+)$", | |
| 22 | + "file": 1, | |
| 23 | + "line": 2, | |
| 24 | + "column": 3, | |
| 25 | + "message": 4 | |
| 26 | + } | |
| 27 | + ] | |
| 28 | + }, | |
| 29 | + { | |
| 30 | + "owner": "ct-failure", | |
| 31 | + "severity": "error", | |
| 32 | + "pattern": [ | |
| 33 | + { | |
| 34 | + "regexp": "^(\\S+) failed on line (\\d+)$", | |
| 35 | + "file": 1, | |
| 36 | + "line": 2 | |
| 37 | + }, | |
| 38 | + { | |
| 39 | + "regexp": "^Reason: (.+)$", | |
| 40 | + "message": 1 | |
| 41 | + } | |
| 42 | + ] | |
| 43 | + }, | |
| 44 | + { | |
| 45 | + "owner": "eunit-failure", | |
| 46 | + "severity": "error", | |
| 47 | + "pattern": [ | |
| 48 | + { | |
| 49 | + "regexp": "^\\S+: .+\\.\\.\\.\\*failed\\*$" | |
| 50 | + }, | |
| 51 | + { | |
| 52 | + "regexp": "^in function .+ \\((.+), line (\\d+)\\)$", | |
| 53 | + "file": 1, | |
| 54 | + "line": 2 | |
| 55 | + }, | |
| 56 | + { | |
| 57 | + "regexp": "^in call from .+$" | |
| 58 | + }, | |
| 59 | + { | |
| 60 | + "regexp": "^\\*\\*error:(.+)$", | |
| 61 | + "message": 1 | |
| 62 | + } | |
| 63 | + ] | |
| 64 | + } | |
| 65 | + ] | |
| 66 | +} |
modified
src/setup-beam.js
+14
−9
@@ -80,6 +80,7 @@ async function installOTP(otpSpec) {
| 80 | 80 | }, |
| 81 | 81 | }) |
| 82 | 82 | core.setOutput('otp-version', otpVersion) |
| 83 | + maybeEnableProblemMatchers('erlang') | |
| 83 | 84 | core.endGroup() |
| 84 | 85 | |
| 85 | 86 | return otpVersion |
@@ -101,7 +102,7 @@ async function maybeInstallElixir(elixirSpec) {
| 101 | 102 | }, |
| 102 | 103 | }) |
| 103 | 104 | core.setOutput('elixir-version', elixirVersion) |
| 104 | − maybeEnableElixirProblemMatchers() | |
| 105 | + maybeEnableProblemMatchers('elixir') | |
| 105 | 106 | core.endGroup() |
| 106 | 107 | |
| 107 | 108 | installed = true |
@@ -110,14 +111,18 @@ async function maybeInstallElixir(elixirSpec) {
| 110 | 111 | return installed |
| 111 | 112 | } |
| 112 | 113 | |
| 113 | −function maybeEnableElixirProblemMatchers() { | |
| 114 | − const disableProblemMatchers = getInput('disable_problem_matchers', false) | |
| 115 | − if (disableProblemMatchers === 'false') { | |
| 116 | − // path.join helps ncc figure this out | |
| 117 | − const elixirMatchers = path.join( | |
| 118 | − `${__dirname}/../matchers/elixir-matchers.json`, | |
| 119 | − ) | |
| 120 | − core.info(`##[add-matcher]${elixirMatchers}`) | |
| 114 | +function problemMatchersEnabled() { | |
| 115 | + return getInput('disable_problem_matchers', false) === 'false' | |
| 116 | +} | |
| 117 | + | |
| 118 | +// path.join with static strings helps ncc resolve and bundle the JSON files | |
| 119 | +function maybeEnableProblemMatchers(language) { | |
| 120 | + if (problemMatchersEnabled()) { | |
| 121 | + const matcherFiles = { | |
| 122 | + erlang: path.join(`${__dirname}/../matchers/erlang-matchers.json`), | |
| 123 | + elixir: path.join(`${__dirname}/../matchers/elixir-matchers.json`), | |
| 124 | + } | |
| 125 | + core.info(`##[add-matcher]${matcherFiles[language]}`) | |
| 121 | 126 | } |
| 122 | 127 | } |
| 123 | 128 |
modified
test/setup-beam.test.js
+94
−0
@@ -6,7 +6,9 @@ import path from 'node:path'
| 6 | 6 | import { describe, it } from 'node:test' |
| 7 | 7 | import * as csv from 'csv-parse/sync' |
| 8 | 8 | import elixirMatchers from '../matchers/elixir-matchers.json' with { type: 'json' } |
| 9 | +import erlangMatchers from '../matchers/erlang-matchers.json' with { type: 'json' } | |
| 9 | 10 | const { problemMatcher } = elixirMatchers |
| 11 | +const { problemMatcher: erlangProblemMatcher } = erlangMatchers | |
| 10 | 12 | |
| 11 | 13 | process.env.NODE_ENV = 'test' |
| 12 | 14 |
@@ -1159,6 +1161,98 @@ describe("Elixir Mix matcher's", () => {
| 1159 | 1161 | }) |
| 1160 | 1162 | }) |
| 1161 | 1163 | |
| 1164 | +describe("Erlang matcher's", () => { | |
| 1165 | + it('erlc errors are properly matched', () => { | |
| 1166 | + const [matcher] = erlangProblemMatcher.find( | |
| 1167 | + ({ owner }) => owner === 'erlc-error', | |
| 1168 | + ).pattern | |
| 1169 | + | |
| 1170 | + const output = 'src/mymod.erl:42:5: head mismatch' | |
| 1171 | + const match = output.match(matcher.regexp) | |
| 1172 | + assert.equal(match[1], 'src/mymod.erl') | |
| 1173 | + assert.equal(match[2], '42') | |
| 1174 | + assert.equal(match[3], '5') | |
| 1175 | + assert.equal(match[4], 'head mismatch') | |
| 1176 | + }) | |
| 1177 | + | |
| 1178 | + it('erlc errors do not match warnings', () => { | |
| 1179 | + const [matcher] = erlangProblemMatcher.find( | |
| 1180 | + ({ owner }) => owner === 'erlc-error', | |
| 1181 | + ).pattern | |
| 1182 | + | |
| 1183 | + const output = 'src/mymod.erl:42:5: Warning: variable X is unused' | |
| 1184 | + const match = output.match(matcher.regexp) | |
| 1185 | + assert.equal(match, null) | |
| 1186 | + }) | |
| 1187 | + | |
| 1188 | + it('erlc warnings are properly matched', () => { | |
| 1189 | + const [matcher] = erlangProblemMatcher.find( | |
| 1190 | + ({ owner }) => owner === 'erlc-warning', | |
| 1191 | + ).pattern | |
| 1192 | + | |
| 1193 | + const output = 'src/mymod.erl:10:3: Warning: variable X is unused' | |
| 1194 | + const match = output.match(matcher.regexp) | |
| 1195 | + assert.equal(match[1], 'src/mymod.erl') | |
| 1196 | + assert.equal(match[2], '10') | |
| 1197 | + assert.equal(match[3], '3') | |
| 1198 | + assert.equal(match[4], 'variable X is unused') | |
| 1199 | + }) | |
| 1200 | + | |
| 1201 | + it('dialyzer warnings are matched via erlc patterns', () => { | |
| 1202 | + const [matcher] = erlangProblemMatcher.find( | |
| 1203 | + ({ owner }) => owner === 'erlc-warning', | |
| 1204 | + ).pattern | |
| 1205 | + | |
| 1206 | + const output = | |
| 1207 | + 'src/mymod.erl:25:1: warning: Function foo/0 has no local return' | |
| 1208 | + const match = output.match(matcher.regexp) | |
| 1209 | + assert.equal(match[1], 'src/mymod.erl') | |
| 1210 | + assert.equal(match[2], '25') | |
| 1211 | + assert.equal(match[3], '1') | |
| 1212 | + assert.equal(match[4], 'Function foo/0 has no local return') | |
| 1213 | + }) | |
| 1214 | + | |
| 1215 | + it('CT failures are properly matched', () => { | |
| 1216 | + const [filePattern, reasonPattern] = erlangProblemMatcher.find( | |
| 1217 | + ({ owner }) => owner === 'ct-failure', | |
| 1218 | + ).pattern | |
| 1219 | + | |
| 1220 | + const firstOutput = 'test/mymod_SUITE.erl failed on line 55' | |
| 1221 | + const secondOutput = 'Reason: {badmatch,{error,timeout}}' | |
| 1222 | + | |
| 1223 | + const fileMatch = firstOutput.match(filePattern.regexp) | |
| 1224 | + assert.equal(fileMatch[1], 'test/mymod_SUITE.erl') | |
| 1225 | + assert.equal(fileMatch[2], '55') | |
| 1226 | + | |
| 1227 | + const reasonMatch = secondOutput.match(reasonPattern.regexp) | |
| 1228 | + assert.equal(reasonMatch[1], '{badmatch,{error,timeout}}') | |
| 1229 | + }) | |
| 1230 | + | |
| 1231 | + it('EUnit failures are properly matched', () => { | |
| 1232 | + const [headerPattern, funcPattern, callPattern, errorPattern] = | |
| 1233 | + erlangProblemMatcher.find( | |
| 1234 | + ({ owner }) => owner === 'eunit-failure', | |
| 1235 | + ).pattern | |
| 1236 | + | |
| 1237 | + const header = 'mymod_test: hello_test....*failed*' | |
| 1238 | + const func = | |
| 1239 | + 'in function mymod_test:hello_test/0 (test/mymod_test.erl, line 12)' | |
| 1240 | + const call = 'in call from mymod_test:hello_test/0' | |
| 1241 | + const error = '**error:{badmatch,false}' | |
| 1242 | + | |
| 1243 | + assert.ok(header.match(headerPattern.regexp)) | |
| 1244 | + | |
| 1245 | + const funcMatch = func.match(funcPattern.regexp) | |
| 1246 | + assert.equal(funcMatch[1], 'test/mymod_test.erl') | |
| 1247 | + assert.equal(funcMatch[2], '12') | |
| 1248 | + | |
| 1249 | + assert.ok(call.match(callPattern.regexp)) | |
| 1250 | + | |
| 1251 | + const errorMatch = error.match(errorPattern.regexp) | |
| 1252 | + assert.equal(errorMatch[1], '{badmatch,false}') | |
| 1253 | + }) | |
| 1254 | +}) | |
| 1255 | + | |
| 1162 | 1256 | function unsimulateInput(key) { |
| 1163 | 1257 | return simulateInput(key, '') |
| 1164 | 1258 | } |
Parents: b3cbb6a