Skip to content

Commit bc32ed5

Browse files
authored
feat(cli): Support GitHub workflow commands (#326)
1 parent 4f7abab commit bc32ed5

File tree

3 files changed

+99
-1
lines changed

3 files changed

+99
-1
lines changed

cmd/rslint/cmd.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"runtime/pprof"
1212
"runtime/trace"
1313
"strconv"
14+
"strings"
1415
"sync"
1516
"time"
1617
"unicode"
@@ -87,11 +88,53 @@ func printDiagnostic(d rule.RuleDiagnostic, w *bufio.Writer, comparePathOptions
8788
printDiagnosticDefault(d, w, comparePathOptions)
8889
case "jsonline":
8990
printDiagnosticJsonLine(d, w, comparePathOptions)
91+
case "github":
92+
printDiagnosticGitHub(d, w, comparePathOptions)
9093
default:
9194
panic("not supported format " + format)
9295
}
9396
}
9497

98+
// print as [Workflow commands for GitHub Actions](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands) format
99+
func printDiagnosticGitHub(d rule.RuleDiagnostic, w *bufio.Writer, comparePathOptions tspath.ComparePathsOptions) {
100+
diagnosticStart := d.Range.Pos()
101+
diagnosticEnd := d.Range.End()
102+
103+
startLine, startColumn := scanner.GetLineAndCharacterOfPosition(d.SourceFile, diagnosticStart)
104+
endLine, endColumn := scanner.GetLineAndCharacterOfPosition(d.SourceFile, diagnosticEnd)
105+
106+
filePath := tspath.ConvertToRelativePath(d.SourceFile.FileName(), comparePathOptions)
107+
output := fmt.Sprintf(
108+
"::%s file=%s,line=%d,endLine=%d,col=%d,endColumn=%d,title=%s::%s\n",
109+
d.Severity.String(),
110+
escapeProperty(filePath),
111+
startLine+1,
112+
endLine+1,
113+
startColumn+1,
114+
endColumn+1,
115+
d.RuleName,
116+
escapeData(d.Message.Description),
117+
)
118+
w.WriteString(output)
119+
}
120+
121+
func escapeData(str string) string {
122+
// https://github.com/biomejs/biome/blob/4416573f4d709047a28407d99381810b7bc7dcc7/crates/biome_diagnostics/src/display_github.rs#L85C4-L85C15
123+
str = strings.ReplaceAll(str, "%", "%25")
124+
str = strings.ReplaceAll(str, "\r", "%0D")
125+
str = strings.ReplaceAll(str, "\n", "%0A")
126+
return str
127+
}
128+
func escapeProperty(str string) string {
129+
// https://github.com/biomejs/biome/blob/4416573f4d709047a28407d99381810b7bc7dcc7/crates/biome_diagnostics/src/display_github.rs#L103
130+
str = strings.ReplaceAll(str, "%", "%25")
131+
str = strings.ReplaceAll(str, "\r", "%0D")
132+
str = strings.ReplaceAll(str, "\n", "%0A")
133+
str = strings.ReplaceAll(str, ":", "%3A")
134+
str = strings.ReplaceAll(str, ",", "%2C")
135+
return str
136+
}
137+
95138
// print as [jsonline](https://jsonlines.org/) format which can be used for lsp
96139
func printDiagnosticJsonLine(d rule.RuleDiagnostic, w *bufio.Writer, comparePathOptions tspath.ComparePathsOptions) {
97140
diagnosticStart := d.Range.Pos()
@@ -316,7 +359,7 @@ Usage:
316359
Options:
317360
--init Initialize a default config in the current directory.
318361
--config PATH Which rslint config file to use. Defaults to rslint.json.
319-
--format FORMAT Output format: default | jsonline
362+
--format FORMAT Output format: default | jsonline | github
320363
--fix Automatically fix problems
321364
--no-color Disable colored output
322365
--force-color Force colored output

packages/rslint-test-tools/rstest.config.mts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ export default defineConfig({
44
testEnvironment: 'node',
55
globals: true,
66
include: [
7+
// cli
8+
'./tests/cli/basic.test.ts',
9+
710
// eslint-plugin-import
811
'./tests/eslint-plugin-import/rules/no-self-import.test.ts',
912

packages/rslint-test-tools/tests/cli/basic.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,58 @@ describe('CLI Configuration Tests', () => {
211211
}
212212
});
213213

214+
test('should output in github workflow format when --format github is used', async () => {
215+
const tempDir = await createTempDir({
216+
'rslint.json': JSON.stringify([
217+
{
218+
language: 'javascript',
219+
files: ['**/*.ts'],
220+
languageOptions: {
221+
parserOptions: {
222+
projectService: false,
223+
project: ['./tsconfig.json'],
224+
},
225+
},
226+
rules: {
227+
'@typescript-eslint/no-unsafe-member-access': 'error',
228+
},
229+
plugins: ['@typescript-eslint'],
230+
},
231+
]),
232+
'tsconfig.json': JSON.stringify({
233+
compilerOptions: {
234+
target: 'ES2020',
235+
module: 'ESNext',
236+
strict: true,
237+
},
238+
include: ['**/*.ts'],
239+
}),
240+
'test:%,\r\nfile.ts': `
241+
let a: any = 10;
242+
a.b = 20;
243+
`,
244+
});
245+
246+
try {
247+
const result = await runRslint(['--format', 'github'], tempDir);
248+
249+
const lines = result.stdout
250+
.trim()
251+
.split('\n')
252+
.filter(line => line.trim());
253+
254+
expect(lines.length).toBe(2);
255+
expect(lines[0]).toBe(
256+
'::error file=test%3A%25%2C%0D%0Afile.ts,line=2,endLine=2,col=16,endColumn=19,title=@typescript-eslint/no-explicit-any::Unexpected any. Specify a different type.',
257+
);
258+
expect(lines[1]).toBe(
259+
'::error file=test%3A%25%2C%0D%0Afile.ts,line=3,endLine=3,col=11,endColumn=12,title=@typescript-eslint/no-unsafe-member-access::Unsafe member access .b on an `any` value.',
260+
);
261+
} finally {
262+
await cleanupTempDir(tempDir);
263+
}
264+
});
265+
214266
test('should only report errors when --quiet flag is used', async () => {
215267
const tempDir = await createTempDir({
216268
'rslint.json': JSON.stringify([

0 commit comments

Comments
 (0)