diff --git a/.changeset/sharp-days-kiss.md b/.changeset/sharp-days-kiss.md new file mode 100644 index 00000000..a8f01e48 --- /dev/null +++ b/.changeset/sharp-days-kiss.md @@ -0,0 +1,5 @@ +--- +'prettier-plugin-sh': minor +--- + +add support for file pragmas diff --git a/packages/sh/package.json b/packages/sh/package.json index 0f8c76f0..608b939a 100644 --- a/packages/sh/package.json +++ b/packages/sh/package.json @@ -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" } diff --git a/packages/sh/src/index.ts b/packages/sh/src/index.ts index 3f3dceb5..3107fb63 100644 --- a/packages/sh/src/index.ts +++ b/packages/sh/src/index.ts @@ -108,6 +108,44 @@ const ShPlugin: Plugin = { } }, 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*(#(?.*))?$/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 => diff --git a/packages/sh/test/parser.spec.ts b/packages/sh/test/parser.spec.ts new file mode 100644 index 00000000..24829a2f --- /dev/null +++ b/packages/sh/test/parser.spec.ts @@ -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() + }) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4e4105a..ea3abdf5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,13 +29,13 @@ importers: version: 0.5.0 '@changesets/cli': specifier: ^2.27.6 - version: 2.27.6 + version: 2.27.7 '@mdx-js/rollup': specifier: ^3.0.1 version: 3.0.1(rollup@4.14.0) '@types/lodash': specifier: ^4.14.202 - version: 4.14.202 + version: 4.17.7 '@types/mvdan-sh': specifier: ^0.10.9 version: 0.10.9 @@ -153,6 +153,13 @@ importers: sh-syntax: specifier: ^0.4.2 version: 0.4.2 + devDependencies: + '@types/common-tags': + specifier: ^1.8.4 + version: 1.8.4 + common-tags: + specifier: ^1.8.2 + version: 1.8.2 packages/sql: dependencies: @@ -170,7 +177,7 @@ importers: version: 15.0.2 tslib: specifier: ^2.6.2 - version: 2.6.2 + version: 2.6.3 packages/toml: dependencies: @@ -290,7 +297,7 @@ packages: npm-run-all: 4.1.5 prettier: 3.2.4 simple-git-hooks: 2.8.1 - tslib: 2.6.2 + tslib: 2.6.3 transitivePeerDependencies: - '@babel/traverse' - '@markuplint/ml-core' @@ -2269,11 +2276,11 @@ packages: resolution: {integrity: sha512-h0OYmPR3A5Dfbetra/GzxBAzQk8sH7LhRkRUTdagX6nrtlUgJGYCTv4bBK33jsTQw9HDd8PE2x1Ma+iRKEDUsw==} dev: true - /@changesets/apply-release-plan@7.0.3: - resolution: {integrity: sha512-klL6LCdmfbEe9oyfLxnidIf/stFXmrbFO/3gT5LU5pcyoZytzJe4gWpTBx3BPmyNPl16dZ1xrkcW7b98e3tYkA==} + /@changesets/apply-release-plan@7.0.4: + resolution: {integrity: sha512-HLFwhKWayKinWAul0Vj+76jVx1Pc2v55MGPVjZ924Y/ROeSsBMFutv9heHmCUj48lJyRfOTJG5+ar+29FUky/A==} dependencies: '@babel/runtime': 7.22.6 - '@changesets/config': 3.0.1 + '@changesets/config': 3.0.2 '@changesets/get-version-range-type': 0.4.0 '@changesets/git': 3.0.0 '@changesets/should-skip-package': 0.1.0 @@ -2288,12 +2295,12 @@ packages: semver: 7.6.0 dev: true - /@changesets/assemble-release-plan@6.0.2: - resolution: {integrity: sha512-n9/Tdq+ze+iUtjmq0mZO3pEhJTKkku9hUxtUadW30jlN7kONqJG3O6ALeXrmc6gsi/nvoCuKjqEJ68Hk8RbMTQ==} + /@changesets/assemble-release-plan@6.0.3: + resolution: {integrity: sha512-bLNh9/Lgl1VwkjWZTq8JmRqH+hj7/Yzfz0jsQ/zJJ+FTmVqmqPj3szeKOri8O/hEM8JmHW019vh2gTO9iq5Cuw==} dependencies: '@babel/runtime': 7.22.6 '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.0 + '@changesets/get-dependents-graph': 2.1.1 '@changesets/should-skip-package': 0.1.0 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 @@ -2316,18 +2323,18 @@ packages: - encoding dev: true - /@changesets/cli@2.27.6: - resolution: {integrity: sha512-PB7KS5JkCQ4WSXlnfThn8CXAHVwYxFdZvYTimhi12fls/tzj9iimUhKsYwkrKSbw1AiVlGCZtihj5Wkt6siIjA==} + /@changesets/cli@2.27.7: + resolution: {integrity: sha512-6lr8JltiiXPIjDeYg4iM2MeePP6VN/JkmqBsVA5XRiy01hGS3y629LtSDvKcycj/w/5Eur1rEwby/MjcYS+e2A==} hasBin: true dependencies: '@babel/runtime': 7.22.6 - '@changesets/apply-release-plan': 7.0.3 - '@changesets/assemble-release-plan': 6.0.2 + '@changesets/apply-release-plan': 7.0.4 + '@changesets/assemble-release-plan': 6.0.3 '@changesets/changelog-git': 0.2.0 - '@changesets/config': 3.0.1 + '@changesets/config': 3.0.2 '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.0 - '@changesets/get-release-plan': 4.0.2 + '@changesets/get-dependents-graph': 2.1.1 + '@changesets/get-release-plan': 4.0.3 '@changesets/git': 3.0.0 '@changesets/logger': 0.1.0 '@changesets/pre': 2.0.0 @@ -2354,11 +2361,11 @@ packages: term-size: 2.2.1 dev: true - /@changesets/config@3.0.1: - resolution: {integrity: sha512-nCr8pOemUjvGJ8aUu8TYVjqnUL+++bFOQHBVmtNbLvKzIDkN/uiP/Z4RKmr7NNaiujIURHySDEGFPftR4GbTUA==} + /@changesets/config@3.0.2: + resolution: {integrity: sha512-cdEhS4t8woKCX2M8AotcV2BOWnBp09sqICxKapgLHf9m5KdENpWjyrFNMjkLqGJtUys9U+w93OxWT0czorVDfw==} dependencies: '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.0 + '@changesets/get-dependents-graph': 2.1.1 '@changesets/logger': 0.1.0 '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 @@ -2372,8 +2379,8 @@ packages: extendable-error: 0.1.7 dev: true - /@changesets/get-dependents-graph@2.1.0: - resolution: {integrity: sha512-QOt6pQq9RVXKGHPVvyKimJDYJumx7p4DO5MO9AhRJYgAPgv0emhNqAqqysSVKHBm4sxKlGN4S1zXOIb5yCFuhQ==} + /@changesets/get-dependents-graph@2.1.1: + resolution: {integrity: sha512-LRFjjvigBSzfnPU2n/AhFsuWR5DK++1x47aq6qZ8dzYsPtS/I5mNhIGAS68IAxh1xjO9BTtz55FwefhANZ+FCA==} dependencies: '@changesets/types': 6.0.0 '@manypkg/get-packages': 1.1.3 @@ -2391,12 +2398,12 @@ packages: - encoding dev: true - /@changesets/get-release-plan@4.0.2: - resolution: {integrity: sha512-rOalz7nMuMV2vyeP7KBeAhqEB7FM2GFPO5RQSoOoUKKH9L6wW3QyPA2K+/rG9kBrWl2HckPVES73/AuwPvbH3w==} + /@changesets/get-release-plan@4.0.3: + resolution: {integrity: sha512-6PLgvOIwTSdJPTtpdcr3sLtGatT+Jr22+cQwEBJBy6wP0rjB4yJ9lv583J9fVpn1bfQlBkDa8JxbS2g/n9lIyA==} dependencies: '@babel/runtime': 7.22.6 - '@changesets/assemble-release-plan': 6.0.2 - '@changesets/config': 3.0.1 + '@changesets/assemble-release-plan': 6.0.3 + '@changesets/config': 3.0.2 '@changesets/pre': 2.0.0 '@changesets/read': 0.6.0 '@changesets/types': 6.0.0 @@ -3674,7 +3681,7 @@ packages: glob: 10.3.10 prettier: 3.2.4 ts-node: 10.9.2(@types/node@20.10.4)(typescript@5.3.3) - tslib: 2.6.2 + tslib: 2.6.3 typescript: 5.3.3 transitivePeerDependencies: - '@swc/core' @@ -3700,7 +3707,7 @@ packages: ignore: 5.3.0 jsonc: 2.0.0 minimatch: 9.0.4 - tslib: 2.6.2 + tslib: 2.6.3 transitivePeerDependencies: - '@types/node' - supports-color @@ -3713,7 +3720,7 @@ packages: '@markuplint/ml-ast': 3.2.0 '@markuplint/parser-utils': 3.13.0 parse5: 7.1.2 - tslib: 2.6.2 + tslib: 2.6.3 type-fest: 4.9.0 transitivePeerDependencies: - supports-color @@ -3725,7 +3732,7 @@ packages: '@markuplint/ml-ast': 3.2.0 '@markuplint/parser-utils': 3.9.0 parse5: 7.1.2 - tslib: 2.6.2 + tslib: 2.6.3 type-fest: 3.12.0 transitivePeerDependencies: - supports-color @@ -3777,7 +3784,7 @@ packages: '@types/debug': 4.1.12 debug: 4.3.4 is-plain-object: 5.0.0 - tslib: 2.6.2 + tslib: 2.6.3 type-fest: 4.9.0 transitivePeerDependencies: - supports-color @@ -3790,7 +3797,7 @@ packages: '@markuplint/types': 3.8.0 dom-accessibility-api: 0.6.3 is-plain-object: 5.0.0 - tslib: 2.6.2 + tslib: 2.6.3 type-fest: 3.12.0 transitivePeerDependencies: - supports-color @@ -3803,7 +3810,7 @@ packages: '@markuplint/types': 3.12.0 dom-accessibility-api: 0.6.3 is-plain-object: 5.0.0 - tslib: 2.6.2 + tslib: 2.6.3 type-fest: 4.9.0 transitivePeerDependencies: - supports-color @@ -3815,7 +3822,7 @@ packages: '@markuplint/ml-ast': 3.2.0 '@markuplint/types': 3.12.0 '@types/uuid': 9.0.7 - tslib: 2.6.2 + tslib: 2.6.3 type-fest: 4.9.0 uuid: 9.0.1 transitivePeerDependencies: @@ -3828,7 +3835,7 @@ packages: '@markuplint/ml-ast': 3.2.0 '@markuplint/types': 3.8.0 '@types/uuid': 9.0.7 - tslib: 2.6.2 + tslib: 2.6.3 type-fest: 3.12.0 uuid: 9.0.1 transitivePeerDependencies: @@ -3849,7 +3856,7 @@ packages: ansi-colors: 4.1.3 chrono-node: 2.7.4 debug: 4.3.4 - tslib: 2.6.2 + tslib: 2.6.3 type-fest: 4.9.0 transitivePeerDependencies: - supports-color @@ -3862,7 +3869,7 @@ packages: '@types/debug': 4.1.12 debug: 4.3.4 postcss-selector-parser: 6.0.15 - tslib: 2.6.2 + tslib: 2.6.3 type-fest: 4.9.0 transitivePeerDependencies: - supports-color @@ -3881,7 +3888,7 @@ packages: '@markuplint/ml-ast': 3.2.0 '@markuplint/parser-utils': 3.9.0 svelte: 3.59.2 - tslib: 2.6.2 + tslib: 2.6.3 transitivePeerDependencies: - supports-color dev: true @@ -3926,7 +3933,7 @@ packages: '@markuplint/html-parser': 3.9.0 '@markuplint/ml-ast': 3.2.0 '@markuplint/parser-utils': 3.9.0 - tslib: 2.6.2 + tslib: 2.6.3 vue-eslint-parser: 9.3.1(eslint@8.44.0) transitivePeerDependencies: - eslint @@ -4076,7 +4083,7 @@ packages: imagemin-upng: 4.0.0 imagemin-webp: 8.0.0 is-glob: 4.0.3 - tslib: 2.6.2 + tslib: 2.6.3 dev: true /@pkgr/rollup@4.1.2(@vue/compiler-sfc@3.4.21): @@ -4107,7 +4114,7 @@ packages: rollup-plugin-unassert: 0.6.0 rollup-plugin-vue: 6.0.0(@vue/compiler-sfc@3.4.21) rollup-plugin-vue-jsx-compat: 0.0.6 - tslib: 2.6.2 + tslib: 2.6.3 unassert: 2.0.2 transitivePeerDependencies: - '@vue/compiler-sfc' @@ -4127,7 +4134,7 @@ packages: is-glob: 4.0.3 open: 9.1.0 picocolors: 1.0.0 - tslib: 2.6.2 + tslib: 2.6.3 dev: true /@polka/url@1.0.0-next.21: @@ -4578,6 +4585,10 @@ packages: resolution: {integrity: sha512-uLK0/0dOYdkX8hNsezpYh1gc8eerbhf9bOKZ3e24sP67703mw9S14/yW6mSTatiaKO9v+mU/a1EVy4rOXXeZTA==} dev: true + /@types/common-tags@1.8.4: + resolution: {integrity: sha512-S+1hLDJPjWNDhcGxsxEbepzaxWqURP/o+3cP4aa2w7yBXgdcmKGQtZzP8JbyfOd0m+33nh+8+kvxYE2UJtBDkg==} + dev: true + /@types/concat-stream@2.0.0: resolution: {integrity: sha512-t3YCerNM7NTVjLuICZo5gYAXYoDvpuuTceCcFQWcDQz26kxUR5uIWolxbIR5jRNIXpMqhOpW/b8imCR1LEmuJw==} dependencies: @@ -4650,8 +4661,8 @@ packages: '@types/node': 20.10.4 dev: true - /@types/lodash@4.14.202: - resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} + /@types/lodash@4.17.7: + resolution: {integrity: sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==} dev: true /@types/mdast@3.0.11: @@ -5319,7 +5330,7 @@ packages: resolution: {integrity: sha512-x9SLf2jNNh3nG+haVIwKX/GVW8PcvSRmkeT9WqTDYSAVuwT9IzwEyVm09FCZpOo/dtFRxE9vaNXqcAf/MIxphg==} engines: {node: '>= 14'} dependencies: - tslib: 2.6.2 + tslib: 2.6.3 dev: true /ansi-colors@4.1.3: @@ -6321,6 +6332,11 @@ packages: engines: {node: '>= 12.0.0'} dev: true + /common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + dev: true + /commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} dev: true @@ -7528,7 +7544,7 @@ packages: remark-parse: 10.0.2 remark-stringify: 10.0.3 synckit: 0.8.5 - tslib: 2.6.2 + tslib: 2.6.3 unified: 10.1.2 unified-engine: 10.1.0 unist-util-visit: 4.1.2 @@ -7719,7 +7735,7 @@ packages: eslint-plugin-utils: 0.3.2(eslint@8.44.0) markuplint: 3.15.0(@types/node@20.10.4)(typescript@5.3.3) synckit: 0.8.5 - tslib: 2.6.2 + tslib: 2.6.3 transitivePeerDependencies: - '@swc/core' - '@swc/wasm' @@ -7741,7 +7757,7 @@ packages: remark-mdx: 2.3.0 remark-parse: 10.0.2 remark-stringify: 10.0.3 - tslib: 2.6.2 + tslib: 2.6.3 unified: 10.1.2 vfile: 5.3.7 transitivePeerDependencies: @@ -10639,7 +10655,7 @@ packages: '@markuplint/parser-utils': 3.13.0 angular-html-parser: 4.0.1 synckit: 0.8.5 - tslib: 2.6.2 + tslib: 2.6.3 transitivePeerDependencies: - supports-color dev: true @@ -10678,7 +10694,7 @@ packages: os-locale: 5.0.0 strict-event-emitter: 0.5.1 strip-ansi: 6.0.1 - tslib: 2.6.2 + tslib: 2.6.3 type-fest: 4.9.0 uuid: 9.0.1 transitivePeerDependencies: @@ -15208,7 +15224,7 @@ packages: /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: - tslib: 2.6.2 + tslib: 2.6.3 dev: true /sade@1.8.1: @@ -15442,7 +15458,7 @@ packages: dependencies: nanoid: 5.0.6 size-limit: 10.0.2 - tslib: 2.6.2 + tslib: 2.6.3 dev: true /size-limit-preset-node-lib@0.3.0(size-limit@11.0.2): @@ -15451,7 +15467,7 @@ packages: dependencies: '@size-limit/file': 10.0.2(size-limit@11.0.2) size-limit-node-esbuild: 0.3.0 - tslib: 2.6.2 + tslib: 2.6.3 transitivePeerDependencies: - size-limit dev: true @@ -16211,7 +16227,7 @@ packages: engines: {node: ^14.18.0 || >=16.0.0} dependencies: '@pkgr/utils': 2.4.2 - tslib: 2.6.2 + tslib: 2.6.3 dev: true /table@6.8.1: @@ -16460,6 +16476,10 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: false + + /tslib@2.6.3: + resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} /tsutils@3.21.0(typescript@5.3.3): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} @@ -16510,7 +16530,7 @@ packages: fast-glob: 3.3.2 minimatch: 9.0.4 normalize-path: 3.0.0 - tslib: 2.6.2 + tslib: 2.6.3 tsutils: 3.21.0(typescript@5.3.3) typescript: 5.3.3 dev: true