Skip to content

Commit 2da5d07

Browse files
feat: GitHub lint formatter #1843 (#1855)
Closes #1843 This adds in a lint formatter which formats the content to be shown as annotations within github. This is particularly useful in pr's as it will indicate within the files view, which files should be updated and it includes the line message. Multi-line content is not supported as per actions/toolkit#193 otherwise we would add the fix command and suggestions to this message. An example of what this would produce using the test data is: _Files tab_ <img width="1536" height="696" alt="image" src="https://github.com/user-attachments/assets/eb70fe34-40b6-41d6-850f-9ea1a871583c" /> _Job Log_ <img width="1506" height="302" alt="image" src="https://github.com/user-attachments/assets/fa0c9149-ead2-4196-a5d3-0f3b99f3adbc" /> _Annotations Panel_ <img width="1534" height="289" alt="image" src="https://github.com/user-attachments/assets/26a72e3e-cf05-43c7-bc83-973c76c37123" /> Note: my github action was called annotations hence why it appears in the title
1 parent 00e9a48 commit 2da5d07

7 files changed

Lines changed: 300 additions & 3 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ Output:
128128
-o, --output-file path::String Enable report to be written to a file.
129129
-f, --format String Use a specific output format.
130130
131-
Available formatter : checkstyle, compact, jslint-xml, json, junit, pretty-error, stylish, table, tap, unix
131+
Available formatter : checkstyle, compact, github, jslint-xml, json, junit, pretty-error, stylish, table, tap, unix
132132
133133
Available formatter for --fix: compats, diff, fixed-result, json, stylish - default: stylish
134134
--no-color Disable color in piped output.
@@ -335,6 +335,7 @@ Use the following formatters:
335335
- stylish (defaults)
336336
- compact
337337
- checkstyle
338+
- github
338339
- jslint-xml
339340
- junit
340341
- tap

docs/cli.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Output:
5252
-o, --output-file path::String Enable report to be written to a file.
5353
-f, --format String Use a specific output format.
5454
55-
Available formatter : checkstyle, compact, jslint-xml, json, junit, pretty-error, stylish, table, tap, unix
55+
Available formatter : checkstyle, compact, github, jslint-xml, json, junit, pretty-error, stylish, table, tap, unix
5656
5757
Available formatter for --fix: compats, diff, fixed-result, json, stylish - default: stylish
5858
--no-color Disable color in piped output.

packages/@textlint/linter-formatter/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ npm install @textlint/linter-formatter
1212

1313
See [formatters/](src/formatters).
1414

15-
Currently, you can use "stylish" (defaults), "checkstyle", "compact", "jslint-xml", "json", "junit", "pretty-error", "table", "tap", and "unix".
15+
Currently, you can use "stylish" (defaults), "checkstyle", "compact", "github", "jslint-xml", "json", "junit", "pretty-error", "table", "tap", and "unix".
1616

1717
```js
1818
const { loadFormatter } = require("@textlint/linter-formatter");
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @fileoverview github-style formatter.
3+
* @author thompson-tomo
4+
* @copyright 2015 James Thompson 2025. All rights reserved.
5+
*/
6+
7+
"use strict";
8+
import type { TextlintResult } from "@textlint/types";
9+
10+
//------------------------------------------------------------------------------
11+
// Helper Functions
12+
//------------------------------------------------------------------------------
13+
14+
/**
15+
* Returns a canonical error level string based upon the error message passed in.
16+
* @param {object} message Individual error message provided by eslint
17+
* @returns {String} Error level string
18+
*/
19+
function getMessageType(message: { fatal?: boolean; severity: number }): string {
20+
if (message.fatal || message.severity === 2) {
21+
return "error";
22+
} else if (message.severity === 1) {
23+
return "warning";
24+
} else if (message.severity === 3) {
25+
return "notice";
26+
} else {
27+
return "warning";
28+
}
29+
}
30+
31+
//------------------------------------------------------------------------------
32+
// Public Interface
33+
//------------------------------------------------------------------------------
34+
35+
function formatter(results: TextlintResult[]) {
36+
let output = "";
37+
38+
results.forEach(function (result) {
39+
const messages = result.messages;
40+
41+
// ::warning file={name},line={line},endLine={endLine},title={title}::{message}
42+
messages.forEach(function (message) {
43+
output += `::${getMessageType(message)} `;
44+
output += `file=${result.filePath},`;
45+
output += `line=${message.loc.start.line || 1},`;
46+
output += `endLine=${message.loc.end.line || message.loc.start.line || 1},`;
47+
output += `col=${message.loc.start.column || 1},`;
48+
output += `endColumn=${message.loc.end.column || message.loc.start.column || 1},`;
49+
output += `title=TextLint${message.ruleId ? `->${message.ruleId}` : ""}::`;
50+
output += `${message.message.trim()}`;
51+
output += "\n";
52+
});
53+
});
54+
55+
return output.trimEnd();
56+
}
57+
58+
export default formatter;

packages/@textlint/linter-formatter/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import debug0 from "debug";
1111
// formatter
1212
import checkstyleFormatter from "./formatters/checkstyle.js";
1313
import compactFormatter from "./formatters/compact.js";
14+
import githubFormatter from "./formatters/github.js";
1415
import jslintXMLFormatter from "./formatters/jslint-xml.js";
1516
import jsonFormatter from "./formatters/json.js";
1617
import junitFormatter from "./formatters/junit.js";
@@ -23,6 +24,7 @@ import unixFormatter from "./formatters/unix.js";
2324
const builtinFormatterList = {
2425
checkstyle: checkstyleFormatter,
2526
compact: compactFormatter,
27+
"github": githubFormatter,
2628
"jslint-xml": jslintXMLFormatter,
2729
json: jsonFormatter,
2830
junit: junitFormatter,
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/**
2+
* @fileoverview Tests for options.
3+
* @author thompson-tomo
4+
*/
5+
6+
import formatter from "../../src/formatters/github";
7+
import { describe, it } from "vitest";
8+
import * as assert from "node:assert";
9+
import type { TextlintResult } from "@textlint/types";
10+
import { createTestMessage, createTestResult } from "../test-helper";
11+
12+
describe("formatter:github", function () {
13+
describe("when passed no messages", function () {
14+
const code: TextlintResult[] = [
15+
createTestResult({
16+
filePath: "foo.js",
17+
messages: []
18+
})
19+
];
20+
21+
it("should return nothing", function () {
22+
const result = formatter(code);
23+
assert.equal(result, "");
24+
});
25+
});
26+
27+
describe("when passed a single message", function () {
28+
const code: TextlintResult[] = [
29+
createTestResult({
30+
filePath: "foo.js",
31+
messages: [
32+
createTestMessage({
33+
message: "Unexpected foo.",
34+
severity: 2,
35+
loc: {
36+
start: {
37+
line: 5,
38+
column: 10
39+
},
40+
end: {
41+
line: 5,
42+
column: 12
43+
}
44+
},
45+
ruleId: "foo"
46+
})
47+
]
48+
})
49+
];
50+
51+
it("should return a string in the github error annotation format", function () {
52+
const result = formatter(code);
53+
assert.equal(
54+
result,
55+
"::error file=foo.js,line=5,endLine=5,col=10,endColumn=12,title=TextLint->foo::Unexpected foo."
56+
);
57+
});
58+
59+
it("should return a string in the github warning annotation format", function () {
60+
code[0].messages[0].severity = 1;
61+
const result = formatter(code);
62+
assert.equal(
63+
result,
64+
"::warning file=foo.js,line=5,endLine=5,col=10,endColumn=12,title=TextLint->foo::Unexpected foo."
65+
);
66+
});
67+
68+
it("should return a string in the github info annotation format", function () {
69+
code[0].messages[0].severity = 3;
70+
const result = formatter(code);
71+
assert.equal(
72+
result,
73+
"::notice file=foo.js,line=5,endLine=5,col=10,endColumn=12,title=TextLint->foo::Unexpected foo."
74+
);
75+
});
76+
});
77+
78+
describe("when passed a fatal error message", function () {
79+
const code: TextlintResult[] = [
80+
createTestResult({
81+
filePath: "foo.js",
82+
messages: [
83+
createTestMessage({
84+
fatal: true,
85+
message: "Unexpected foo.",
86+
loc: {
87+
start: {
88+
line: 5,
89+
column: 10
90+
},
91+
end: {
92+
line: 5,
93+
column: 12
94+
}
95+
},
96+
ruleId: "foo"
97+
})
98+
]
99+
})
100+
];
101+
102+
it("should return a string in the github error annotation format", function () {
103+
const result = formatter(code);
104+
assert.equal(
105+
result,
106+
"::error file=foo.js,line=5,endLine=5,col=10,endColumn=12,title=TextLint->foo::Unexpected foo."
107+
);
108+
});
109+
});
110+
111+
describe("when passed multiple messages", function () {
112+
const code: TextlintResult[] = [
113+
createTestResult({
114+
filePath: "foo.js",
115+
messages: [
116+
createTestMessage({
117+
message: "Unexpected foo.",
118+
severity: 2,
119+
loc: {
120+
start: {
121+
line: 5,
122+
column: 10
123+
},
124+
end: {
125+
line: 5,
126+
column: 12
127+
}
128+
},
129+
ruleId: "foo"
130+
}),
131+
createTestMessage({
132+
message: "Unexpected bar.",
133+
severity: 1,
134+
loc: {
135+
start: {
136+
line: 6,
137+
column: 14
138+
},
139+
end: {
140+
line: 6,
141+
column: 16
142+
}
143+
},
144+
ruleId: "bar"
145+
})
146+
]
147+
})
148+
];
149+
150+
it("should return a string with multiple entries", function () {
151+
const result = formatter(code);
152+
assert.equal(
153+
result,
154+
"::error file=foo.js,line=5,endLine=5,col=10,endColumn=12,title=TextLint->foo::Unexpected foo.\n::warning file=foo.js,line=6,endLine=6,col=14,endColumn=16,title=TextLint->bar::Unexpected bar."
155+
);
156+
});
157+
});
158+
159+
describe("when passed multiple files with 1 message each", function () {
160+
const code: TextlintResult[] = [
161+
createTestResult({
162+
filePath: "foo.js",
163+
messages: [
164+
createTestMessage({
165+
message: "Unexpected foo.",
166+
severity: 2,
167+
loc: {
168+
start: {
169+
line: 5,
170+
column: 10
171+
},
172+
end: {
173+
line: 5,
174+
column: 12
175+
}
176+
},
177+
ruleId: "foo"
178+
})
179+
]
180+
}),
181+
createTestResult({
182+
filePath: "bar.js",
183+
messages: [
184+
createTestMessage({
185+
message: "Unexpected bar.",
186+
severity: 1,
187+
loc: {
188+
start: {
189+
line: 6,
190+
column: 14
191+
},
192+
end: {
193+
line: 6,
194+
column: 16
195+
}
196+
},
197+
ruleId: "bar"
198+
})
199+
]
200+
})
201+
];
202+
203+
it("should return a string with multiple entries", function () {
204+
const result = formatter(code);
205+
assert.equal(
206+
result,
207+
"::error file=foo.js,line=5,endLine=5,col=10,endColumn=12,title=TextLint->foo::Unexpected foo.\n::warning file=bar.js,line=6,endLine=6,col=14,endColumn=16,title=TextLint->bar::Unexpected bar."
208+
);
209+
});
210+
});
211+
212+
describe("when passed one file not found message", function () {
213+
const code: TextlintResult[] = [
214+
createTestResult({
215+
filePath: "foo.js",
216+
messages: [
217+
createTestMessage({
218+
fatal: true,
219+
message: "Couldn't find foo.js.",
220+
line: 0,
221+
column: 0
222+
})
223+
]
224+
})
225+
];
226+
227+
it("should return a string without line and column", function () {
228+
const result = formatter(code);
229+
assert.equal(
230+
result,
231+
"::error file=foo.js,line=1,endLine=1,col=1,endColumn=1,title=TextLint::Couldn't find foo.js."
232+
);
233+
});
234+
});
235+
});

packages/@textlint/linter-formatter/test/textlint-formatter.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ describe("@textlint/linter-formatter-test", function () {
5757
assert.deepEqual(getFormatterList(), [
5858
{ name: "checkstyle" },
5959
{ name: "compact" },
60+
{ name: "github" },
6061
{ name: "jslint-xml" },
6162
{ name: "json" },
6263
{ name: "junit" },

0 commit comments

Comments
 (0)