Skip to content

Commit 1d6c25c

Browse files
committed
Add support for parsing ShellCheck directives
1 parent 217ef05 commit 1d6c25c

File tree

2 files changed

+228
-0
lines changed

2 files changed

+228
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { parseShellCheckDirective } from '../directive'
2+
3+
describe('parseShellCheckDirective', () => {
4+
it('parses a disable directive', () => {
5+
expect(parseShellCheckDirective('# shellcheck disable=SC1000')).toEqual([
6+
{
7+
type: 'disable',
8+
rules: ['SC1000'],
9+
},
10+
])
11+
})
12+
13+
it('parses a disable directive with multiple args', () => {
14+
expect(parseShellCheckDirective('# shellcheck disable=SC1000,SC1001')).toEqual([
15+
{
16+
type: 'disable',
17+
rules: ['SC1000', 'SC1001'],
18+
},
19+
])
20+
21+
expect(
22+
parseShellCheckDirective(
23+
'# shellcheck disable=SC1000,SC2000-SC2002,SC1001 # this is a comment',
24+
),
25+
).toEqual([
26+
{
27+
type: 'disable',
28+
rules: ['SC1000', 'SC2000', 'SC2001', 'SC2002', 'SC1001'],
29+
},
30+
])
31+
32+
expect(parseShellCheckDirective('# shellcheck disable=SC1000,SC1001')).toEqual([
33+
{
34+
type: 'disable',
35+
rules: ['SC1000', 'SC1001'],
36+
},
37+
])
38+
39+
expect(parseShellCheckDirective('# shellcheck disable=SC1000,SC1001')).toEqual([
40+
{
41+
type: 'disable',
42+
rules: ['SC1000', 'SC1001'],
43+
},
44+
])
45+
})
46+
47+
// SC1000-SC9999
48+
it('parses a disable directive with a range', () => {
49+
expect(parseShellCheckDirective('# shellcheck disable=SC1000-SC1005')).toEqual([
50+
{
51+
type: 'disable',
52+
rules: ['SC1000', 'SC1001', 'SC1002', 'SC1003', 'SC1004', 'SC1005'],
53+
},
54+
])
55+
})
56+
57+
it('parses a disable directive with all', () => {
58+
expect(parseShellCheckDirective('# shellcheck disable=all')).toEqual([
59+
{
60+
type: 'disable',
61+
rules: ['all'],
62+
},
63+
])
64+
})
65+
66+
it('parses an enable directive', () => {
67+
expect(
68+
parseShellCheckDirective('# shellcheck enable=require-variable-braces'),
69+
).toEqual([
70+
{
71+
type: 'enable',
72+
rules: ['require-variable-braces'],
73+
},
74+
])
75+
})
76+
77+
it('parses source directive', () => {
78+
expect(parseShellCheckDirective('# shellcheck source=foo.sh')).toEqual([
79+
{
80+
type: 'source',
81+
path: 'foo.sh',
82+
},
83+
])
84+
85+
expect(parseShellCheckDirective('# shellcheck source=/dev/null # a comment')).toEqual(
86+
[
87+
{
88+
type: 'source',
89+
path: '/dev/null',
90+
},
91+
],
92+
)
93+
})
94+
95+
it('parses source-path directive', () => {
96+
expect(parseShellCheckDirective('# shellcheck source-path=src/examples')).toEqual([
97+
{
98+
type: 'source-path',
99+
path: 'src/examples',
100+
},
101+
])
102+
103+
expect(parseShellCheckDirective('# shellcheck source-path=SCRIPTDIR')).toEqual([
104+
{
105+
type: 'source-path',
106+
path: 'SCRIPTDIR',
107+
},
108+
])
109+
})
110+
111+
it('supports multiple directives on the same line', () => {
112+
expect(
113+
parseShellCheckDirective(
114+
`# shellcheck cats=dogs disable=SC1234,SC2345 enable="foo" shell=bash`,
115+
),
116+
).toEqual([
117+
{
118+
type: 'disable',
119+
rules: ['SC1234', 'SC2345'],
120+
},
121+
{
122+
type: 'enable',
123+
rules: ['"foo"'],
124+
},
125+
{
126+
type: 'shell',
127+
shell: 'bash',
128+
},
129+
])
130+
})
131+
132+
it('parses a line with no directive', () => {
133+
expect(parseShellCheckDirective('# foo bar')).toEqual([])
134+
})
135+
})

server/src/shellcheck/directive.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
const DIRECTIVE_TYPES = ['enable', 'disable', 'source', 'source-path', 'shell'] as const
2+
type DirectiveType = (typeof DIRECTIVE_TYPES)[number]
3+
4+
type Directive =
5+
| {
6+
type: 'enable'
7+
rules: string[]
8+
}
9+
| {
10+
type: 'disable'
11+
rules: string[]
12+
}
13+
| {
14+
type: 'source'
15+
path: string
16+
}
17+
| {
18+
type: 'source-path'
19+
path: string
20+
}
21+
| {
22+
type: 'shell'
23+
shell: string
24+
}
25+
26+
const DIRECTIVE_REG_EXP = /^(#\s*shellcheck\s+)([^#]*)/
27+
28+
export function parseShellCheckDirective(line: string): Directive[] {
29+
const match = line.match(DIRECTIVE_REG_EXP)
30+
31+
if (!match) {
32+
return []
33+
}
34+
35+
const commands = match[2]
36+
.split(' ')
37+
.map((command) => command.trim())
38+
.filter((command) => command !== '')
39+
40+
const directives: Directive[] = []
41+
42+
for (const command of commands) {
43+
const [typeKey, directiveValue] = command.split('=')
44+
const type = DIRECTIVE_TYPES.includes(typeKey as any)
45+
? (typeKey as DirectiveType)
46+
: null
47+
48+
if (!type) {
49+
continue
50+
}
51+
52+
if (type === 'source-path' || type === 'source') {
53+
directives.push({
54+
type,
55+
path: directiveValue,
56+
})
57+
} else if (type === 'shell') {
58+
directives.push({
59+
type,
60+
shell: directiveValue,
61+
})
62+
continue
63+
} else if (type === 'enable' || type === 'disable') {
64+
const rules = []
65+
66+
for (const arg of directiveValue.split(',')) {
67+
const ruleRangeMatch = arg.match(/^SC(\d*)-SC(\d*)$/)
68+
if (ruleRangeMatch) {
69+
for (
70+
let i = parseInt(ruleRangeMatch[1], 10);
71+
i <= parseInt(ruleRangeMatch[2], 10);
72+
i++
73+
) {
74+
rules.push(`SC${i}`)
75+
}
76+
} else {
77+
arg
78+
.split(',')
79+
.map((arg) => arg.trim())
80+
.filter((arg) => arg !== '')
81+
.forEach((arg) => rules.push(arg))
82+
}
83+
}
84+
85+
directives.push({
86+
type,
87+
rules,
88+
})
89+
}
90+
}
91+
92+
return directives
93+
}

0 commit comments

Comments
 (0)