feat: add Erlang problem matchers (#433)

cfafb69 · Daniel Widgren · 2026-03-14 16:43

5 files +254 -18
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