Skip to content

Commit e02bd2f

Browse files
feat(sh): add support for file pragmas (#378)
1 parent 09382e2 commit e02bd2f

5 files changed

Lines changed: 179 additions & 0 deletions

File tree

.changeset/sharp-days-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'prettier-plugin-sh': minor
3+
---
4+
5+
add support for file pragmas

packages/sh/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
"mvdan-sh": "^0.10.1",
4141
"sh-syntax": "^0.4.2"
4242
},
43+
"devDependencies": {
44+
"@types/common-tags": "^1.8.4",
45+
"common-tags": "^1.8.2"
46+
},
4347
"publishConfig": {
4448
"access": "public"
4549
}

packages/sh/src/index.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,44 @@ const ShPlugin: Plugin<Node | ShSyntaxNode> = {
108108
}
109109
},
110110
astFormat: 'sh',
111+
hasPragma: (text: string): boolean => {
112+
// We don't want to parse every file twice but Prettier's interface
113+
// isn't conducive to caching/memoizing an upstream Parser, so we're
114+
// going with some minor Regex hackery.
115+
//
116+
// Only read empty lines, comments, and shebangs at the start of the file.
117+
// We do not support Bash's pseudo-block comments.
118+
119+
// No, we don't support unofficial block comments.
120+
const commentLineRegex = /^\s*(#(?<comment>.*))?$/gm
121+
let lastIndex = -1
122+
123+
// Only read leading comments, skip shebangs, and check for the pragma.
124+
// We don't want to have to parse every file twice.
125+
for (;;) {
126+
const match = commentLineRegex.exec(text)
127+
128+
// Found "real" content, EoF, or stuck in a loop.
129+
if (match == null || match.index !== lastIndex + 1) {
130+
return false
131+
}
132+
133+
lastIndex = commentLineRegex.lastIndex
134+
const comment = match.groups?.comment?.trim()
135+
136+
// Empty lines and shebangs have no captures
137+
if (comment == null) {
138+
continue
139+
}
140+
141+
if (
142+
comment.startsWith('@prettier') ||
143+
comment.startsWith('@format')
144+
) {
145+
return true
146+
}
147+
}
148+
},
111149
locStart: node =>
112150
isFunction(node.Pos) ? node.Pos().Offset() : node.Pos.Offset,
113151
locEnd: node =>

packages/sh/test/parser.spec.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { stripIndent } from 'common-tags'
2+
import { describe, it, assert, expect } from 'vitest'
3+
4+
import ShPlugin from 'prettier-plugin-sh'
5+
6+
describe('parser', () => {
7+
const hasPragma = ShPlugin.parsers?.sh?.hasPragma
8+
assert(hasPragma != null)
9+
10+
describe('should detect pragmas', () => {
11+
it('at the top of the file', () => {
12+
expect(
13+
hasPragma(stripIndent`
14+
# @prettier
15+
FOO="bar"
16+
`),
17+
).toBeTruthy()
18+
})
19+
20+
it('with extra leading spaces', () => {
21+
expect(
22+
hasPragma(stripIndent`
23+
# @prettier
24+
FOO="bar"
25+
`),
26+
).toBeTruthy()
27+
})
28+
29+
it('with no leading space', () => {
30+
expect(
31+
hasPragma(stripIndent`
32+
#@prettier
33+
FOO="bar"
34+
`),
35+
).toBeTruthy()
36+
})
37+
38+
it('with "format" pragma instead', () => {
39+
expect(
40+
hasPragma(stripIndent`
41+
# @format
42+
FOO="bar"
43+
`),
44+
).toBeTruthy()
45+
})
46+
47+
it('after leading whitespace', () => {
48+
expect(
49+
hasPragma(stripIndent`
50+
51+
52+
# @prettier
53+
FOO="bar"
54+
`),
55+
).toBeTruthy()
56+
})
57+
58+
it('after leading comments', () => {
59+
expect(
60+
hasPragma(stripIndent`
61+
# Testing!
62+
63+
#
64+
#
65+
# @prettier
66+
FOO="bar"
67+
`),
68+
).toBeTruthy()
69+
})
70+
71+
it('after a shebang', () => {
72+
expect(
73+
hasPragma(stripIndent`
74+
#!/bin/bash
75+
#
76+
77+
# @prettier
78+
FOO="bar"
79+
`),
80+
).toBeTruthy()
81+
})
82+
83+
it('unless none exist', () => {
84+
expect(
85+
hasPragma(stripIndent`
86+
FOO="bar"
87+
`),
88+
).toBeFalsy()
89+
})
90+
91+
it('unless the file is empty', () => {
92+
expect(hasPragma('')).toBeFalsy()
93+
})
94+
95+
it('unless it comes after real content', () => {
96+
expect(
97+
hasPragma(stripIndent`
98+
FOO="bar"
99+
# @prettier
100+
`),
101+
).toBeFalsy()
102+
})
103+
104+
it('unless it comes after real content and comments', () => {
105+
expect(
106+
hasPragma(stripIndent`
107+
108+
# Test
109+
#!
110+
FOO="bar"
111+
# @prettier
112+
`),
113+
).toBeFalsy()
114+
})
115+
})
116+
})

pnpm-lock.yaml

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)