Skip to content

Commit cfafb69

Browse files
authored
feat: add Erlang problem matchers (#433)
* feat: add Erlang problem matchers for erlc, dialyzer, CT, and EUnit Adds GitHub Actions problem matchers for Erlang tooling, matching the existing Elixir matcher support. Covers erlc errors/warnings (which also catches dialyzer output), Common Test failures, and EUnit failures. Closes #390 * chore: rebuild dist bundle and format test file * refactor: move Erlang matchers into installOTP function Moves maybeEnableErlangProblemMatchers() call inside installOTP(), between setOutput and endGroup, matching the Elixir pattern. * refactor: extract generic maybeEnableProblemMatchers function Replaces separate maybeEnableErlangProblemMatchers and maybeEnableElixirProblemMatchers with a single generic maybeEnableProblemMatchers(language) and a problemMatchersEnabled() helper. * fix: use static paths for ncc to resolve matcher JSON files ncc cannot trace dynamic template literals, so the matcher JSON files were not being copied to dist/. Use a static map of paths instead.
1 parent b3cbb6a commit cfafb69

File tree

5 files changed

+254
-18
lines changed

5 files changed

+254
-18
lines changed

dist/erlang-matchers.json

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

dist/index.js

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57588,6 +57588,7 @@ async function installOTP(otpSpec) {
5758857588
},
5758957589
})
5759057590
setOutput('otp-version', otpVersion)
57591+
maybeEnableProblemMatchers('erlang')
5759157592
endGroup()
5759257593

5759357594
return otpVersion
@@ -57609,7 +57610,7 @@ async function maybeInstallElixir(elixirSpec) {
5760957610
},
5761057611
})
5761157612
setOutput('elixir-version', elixirVersion)
57612-
maybeEnableElixirProblemMatchers()
57613+
maybeEnableProblemMatchers('elixir')
5761357614
endGroup()
5761457615

5761557616
installed = true
@@ -57618,14 +57619,18 @@ async function maybeInstallElixir(elixirSpec) {
5761857619
return installed
5761957620
}
5762057621

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]}`)
5762957634
}
5763057635
}
5763157636

matchers/erlang-matchers.json

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

src/setup-beam.js

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ async function installOTP(otpSpec) {
8080
},
8181
})
8282
core.setOutput('otp-version', otpVersion)
83+
maybeEnableProblemMatchers('erlang')
8384
core.endGroup()
8485

8586
return otpVersion
@@ -101,7 +102,7 @@ async function maybeInstallElixir(elixirSpec) {
101102
},
102103
})
103104
core.setOutput('elixir-version', elixirVersion)
104-
maybeEnableElixirProblemMatchers()
105+
maybeEnableProblemMatchers('elixir')
105106
core.endGroup()
106107

107108
installed = true
@@ -110,14 +111,18 @@ async function maybeInstallElixir(elixirSpec) {
110111
return installed
111112
}
112113

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]}`)
121126
}
122127
}
123128

test/setup-beam.test.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import path from 'node:path'
66
import { describe, it } from 'node:test'
77
import * as csv from 'csv-parse/sync'
88
import elixirMatchers from '../matchers/elixir-matchers.json' with { type: 'json' }
9+
import erlangMatchers from '../matchers/erlang-matchers.json' with { type: 'json' }
910
const { problemMatcher } = elixirMatchers
11+
const { problemMatcher: erlangProblemMatcher } = erlangMatchers
1012

1113
process.env.NODE_ENV = 'test'
1214

@@ -1159,6 +1161,98 @@ describe("Elixir Mix matcher's", () => {
11591161
})
11601162
})
11611163

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+
11621256
function unsimulateInput(key) {
11631257
return simulateInput(key, '')
11641258
}

0 commit comments

Comments
 (0)