Skip to content

Commit

Permalink
feat(sh): add support for file pragmas
Browse files Browse the repository at this point in the history
Closes un-ts#377
  • Loading branch information
Kenneth-Sills committed Aug 29, 2024
1 parent a818d8f commit 6a3e2a9
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 55 deletions.
5 changes: 5 additions & 0 deletions .changeset/sharp-days-kiss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'prettier-plugin-sh': minor
---

add support for file pragmas
4 changes: 4 additions & 0 deletions packages/sh/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
"mvdan-sh": "^0.10.1",
"sh-syntax": "^0.4.2"
},
"devDependencies": {
"@types/common-tags": "^1.8.4",
"common-tags": "^1.8.2"
},
"publishConfig": {
"access": "public"
}
Expand Down
38 changes: 38 additions & 0 deletions packages/sh/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,44 @@ const ShPlugin: Plugin<Node | ShSyntaxNode> = {
}
},
astFormat: 'sh',
hasPragma: (text: string): boolean => {
// We don't want to parse every file twice but Prettier's interface
// isn't conducive to caching/memoizing an upstream Parser, so we're
// going with some minor Regex hackery.
//
// Only read empty lines, comments, and shebangs at the start of the file.
// We do not support Bash's pseudo-block comments.

// No, we don't support unofficial block comments.
const commentLineRegex = /^\s*(#(?<comment>.*))?$/gm
let lastIndex = -1

// Only read leading comments, skip shebangs, and check for the pragma.
// We don't want to have to parse every file twice.
for (;;) {
const match = commentLineRegex.exec(text)

// Found "real" content, EoF, or stuck in a loop.
if (match == null || match.index !== lastIndex + 1) {
return false
}

lastIndex = commentLineRegex.lastIndex
const comment = match.groups?.comment?.trim()

// Empty lines and shebangs have no captures
if (comment == null) {
continue
}

if (
comment.startsWith('@prettier') ||
comment.startsWith('@format')
) {
return true
}
}
},
locStart: node =>
isFunction(node.Pos) ? node.Pos().Offset() : node.Pos.Offset,
locEnd: node =>
Expand Down
116 changes: 116 additions & 0 deletions packages/sh/test/parser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { stripIndent } from 'common-tags'
import { describe, it, assert, expect } from 'vitest'

import ShPlugin from 'prettier-plugin-sh'

describe('parser', () => {
const hasPragma = ShPlugin.parsers?.sh?.hasPragma
assert(hasPragma != null)

describe('should detect pragmas', () => {
it('at the top of the file', () => {
expect(
hasPragma(stripIndent`
# @prettier
FOO="bar"
`),
).toBeTruthy()
})

it('with extra leading spaces', () => {
expect(
hasPragma(stripIndent`
# @prettier
FOO="bar"
`),
).toBeTruthy()
})

it('with no leading space', () => {
expect(
hasPragma(stripIndent`
#@prettier
FOO="bar"
`),
).toBeTruthy()
})

it('with "format" pragma instead', () => {
expect(
hasPragma(stripIndent`
# @format
FOO="bar"
`),
).toBeTruthy()
})

it('after leading whitespace', () => {
expect(
hasPragma(stripIndent`
# @prettier
FOO="bar"
`),
).toBeTruthy()
})

it('after leading comments', () => {
expect(
hasPragma(stripIndent`
# Testing!
#
#
# @prettier
FOO="bar"
`),
).toBeTruthy()
})

it('after a shebang', () => {
expect(
hasPragma(stripIndent`
#!/bin/bash
#
# @prettier
FOO="bar"
`),
).toBeTruthy()
})

it('unless none exist', () => {
expect(
hasPragma(stripIndent`
FOO="bar"
`),
).toBeFalsy()
})

it('unless the file is empty', () => {
expect(hasPragma('')).toBeFalsy()
})

it('unless it comes after real content', () => {
expect(
hasPragma(stripIndent`
FOO="bar"
# @prettier
`),
).toBeFalsy()
})

it('unless it comes after real content and comments', () => {
expect(
hasPragma(stripIndent`
# Test
#!
FOO="bar"
# @prettier
`),
).toBeFalsy()
})
})
})
Loading

0 comments on commit 6a3e2a9

Please sign in to comment.