@eslint-sh/eslint-plugin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mike North
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # @eslint-sh/eslint-plugin
2
+
3
+ Shell script parsing and syntax-error reporting for ESLint, with custom-rule authoring over a
4
+ typed shell AST. Provides the `ShellLanguage` / `ShellSourceCode` core that lets ESLint parse and
5
+ traverse `.sh`/`.bash` files, backed by `sh-ast`.
6
+
7
+ **Roadmap:** built-in lint rules and shared configs are planned but not implemented yet.
8
+
9
+ ## `languageOptions` reference
10
+
11
+ | Option | Allowed values | Default | Effect |
12
+ | ----------------- | ----------------------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `dialect` | `"bash"`, `"posix"`, `"mksh"`, `"bats"`, `"zsh"` | `"bash"` | Selects which shell grammar the parser accepts; parsing a construct outside the chosen dialect produces a parse error. |
14
+ | `shellVersion` | A numeric `MAJOR` or `MAJOR.MINOR` string, e.g. `"5"` or `"3.2"` | unset | Reserved for the rule layer (e.g. a future bash-3.2 compatibility check); has no effect on parsing today since no rules ship yet. |
15
+ | `directivePrefix` | Lowercase, hyphen-separated words, e.g. `"eslint"` or `"shellcheck-sh"` | `"eslint"` | The prefix directive/inline-config comments must start with — e.g. the default recognizes `# eslint-disable-next-line ...`. |
16
+
17
+ This release reports shell syntax (parse) errors only — no lint rules ship yet, so a "0 problems"
18
+ result means the script parsed without a syntax error, not that it is problem-free.
19
+
20
+ ```js
21
+ // eslint.config.mjs
22
+ import { defineConfig } from 'eslint/config';
23
+ import sh from '@eslint-sh/eslint-plugin'; // or the umbrella: import sh from "eslint-sh"
24
+
25
+ export default defineConfig([
26
+ {
27
+ files: ['**/*.sh', '**/*.bash'],
28
+ plugins: { sh },
29
+ language: 'sh/sh',
30
+ languageOptions: { dialect: 'bash', shellVersion: '3.2' },
31
+ },
32
+ ]);
33
+ ```
34
+
35
+ ## Status
36
+
37
+ `ShellLanguage`/`ShellSourceCode` core, plus the six launch esquery selector classes — see the
38
+ [selector cookbook](./docs/selectors.md) (shipped with this package) for ready-made selector
39
+ recipes. Rules and shared `configs` are not implemented yet — `plugin.rules` is intentionally
40
+ empty here. Disable directives / inline config are implemented and live: all four
41
+ `# eslint-disable*` comment forms, unused-directive detection, and `linterOptions.noInlineConfig`
42
+ interplay work through ESLint's native machinery. No lint rules ship with this package yet, so the
43
+ rule id below is illustrative — but once you register a custom rule of your own, the directives
44
+ suppress it: for a rule you've configured as `sh/your-rule`,
45
+ `# eslint-disable-next-line sh/your-rule -- justification` suppresses it on the following line.
46
+
47
+ ## Public API
48
+
49
+ - `plugin` (default export) — `{ meta: { name, version, namespace: "sh" }, languages: { sh:
50
+ ShellLanguage }, rules: {} }`. `meta.version` always matches this package's `package.json`.
51
+ - `ShellLanguage` — the `@eslint/core` `Language` implementation: `fileType: "text"`,
52
+ `lineStart`/`columnStart: 1`, `nodeTypeKey: "type"`, the bridge's generated `visitorKeys`,
53
+ `validateLanguageOptions`/`normalizeLanguageOptions` for the
54
+ `{dialect, shellVersion, directivePrefix}` schema, `parse`/`createSourceCode`, and
55
+ `matchesSelectorClass` for the six launch esquery selector classes (`:command`, `:loop`,
56
+ `:conditional`, `:expansion`, `:quoted`, `:function` — see the
57
+ [selector cookbook](./docs/selectors.md)).
58
+ - `ShellSourceCode` — extends `@eslint/plugin-kit`'s `TextSourceCodeBase`; implements `traverse()`
59
+ (driven by the bridge's `visitorKeys`, yielding two-phase `VisitNodeStep`s), `getParent()`, and
60
+ the disable-directive surface (`getDisableDirectives()`, `getInlineConfigNodes()`,
61
+ `applyInlineConfig()`).
62
+ - `ShellLanguageOptions` / `NormalizedShellLanguageOptions` / `ShellLanguageOptionsJSON` — the
63
+ `languageOptions` types (`dialect`, `shellVersion`, `directivePrefix`). `directivePrefix` is the
64
+ prefix directive/inline-config comments must start with — e.g. the default `"eslint"` recognizes
65
+ `# eslint-disable-next-line ...`; consumers who already overload `# eslint-*` comments for
66
+ something else can configure a different prefix (e.g. `directivePrefix: 'shellcheck-sh'`).
67
+
68
+ Parse failures surface as fatal lint messages at mvdan/sh's real reported position (never a 1:1
69
+ placeholder) — see `ShParseError` in `sh-ast`.
70
+
71
+ ## Writing your own rules
72
+
73
+ Want to write a custom rule against the shell AST? See the [custom-rule
74
+ tutorial](./docs/custom-rule-tutorial.md) for a complete, CI-tested walkthrough: a selector from
75
+ the cookbook, a full `{ meta, create(context) }` rule module with `context.report`, and the
76
+ flat-config wiring to run it. `docs/` ships with this package (see `package.json#files`), so the
77
+ tutorial and its example files (`docs/examples/`) are available in every installed copy — not just
78
+ on GitHub.
@@ -0,0 +1,317 @@
1
+ import type { Directive } from '@eslint/core';
2
+ import type { File as File_2 } from '@eslint/core';
3
+ import type { FileProblem } from '@eslint/core';
4
+ import type { Language } from '@eslint/core';
5
+ import type { OkParseResult } from '@eslint/core';
6
+ import type { ParseResult } from '@eslint/core';
7
+ import type { RulesConfig } from '@eslint/core';
8
+ import type { ShellDialect } from 'sh-ast';
9
+ import type { ShFile } from 'sh-ast';
10
+ import type { ShNode } from 'sh-ast';
11
+ import type { ShNodes } from 'sh-ast';
12
+ import type { SourceLocation } from '@eslint/core';
13
+ import { TextSourceCodeBase } from '@eslint/plugin-kit';
14
+ import type { TraversalStep } from '@eslint/core';
15
+
16
+ /**
17
+ * The `@eslint-sh/eslint-plugin` plugin object (design/PACKAGES.md,
18
+ * `@eslint-sh/eslint-plugin`): `languages.sh` provides `ShellLanguage` /
19
+ * `ShellSourceCode` (design/ARCHITECTURE.md, "SourceCode and Language
20
+ * objects"), selected in a flat config via `language: "sh/sh"`
21
+ * (`namespace: "sh"`). `meta.version` is read from this package's own
22
+ * `package.json` at runtime — never hardcoded (see `version.ts`).
23
+ *
24
+ * `rules` is empty at this milestone (M2 — `ShellLanguage`/`ShellSourceCode`
25
+ * core, plus the six launch esquery selector classes); rules and shared
26
+ * `configs` are Milestone 3 (design/MILESTONES.md). Adding them later is
27
+ * additive, not a breaking change to this object's shape.
28
+ *
29
+ * Re-exported, unchanged, as the default export of the `eslint-sh` umbrella
30
+ * package (design/PACKAGES.md's Naming decision): importing this package or
31
+ * the umbrella package both yield the same plugin object.
32
+ */
33
+ declare const _default: {
34
+ meta: {
35
+ name: string;
36
+ version: string;
37
+ namespace: string;
38
+ };
39
+ languages: {
40
+ sh: ShellLanguage;
41
+ };
42
+ rules: {};
43
+ };
44
+ export default _default;
45
+
46
+ /**
47
+ * The result of {@link ShellLanguage.normalizeLanguageOptions}: `dialect` and
48
+ * `directivePrefix` are always resolved to concrete values (defaulted when
49
+ * the caller didn't specify them), and `toJSON()` provides the "custom
50
+ * serialization" ESLint's languages guide recommends
51
+ * (https://eslint.org/docs/latest/extend/languages, "normalizeLanguageOptions")
52
+ * so that config caching keyed off `JSON.stringify(languageOptions)` is
53
+ * stable irrespective of the input object's own key order — `JSON.stringify`
54
+ * calls `toJSON()` automatically when present.
55
+ *
56
+ * @public
57
+ */
58
+ export declare interface NormalizedShellLanguageOptions extends Record<string, unknown> {
59
+ readonly dialect: ShellDialect;
60
+ readonly directivePrefix: string;
61
+ readonly shellVersion?: string;
62
+ toJSON(): ShellLanguageOptionsJSON;
63
+ }
64
+
65
+ /**
66
+ * `ShellLanguage` — the `@eslint/core` `Language` implementation for shell
67
+ * scripts (design/ARCHITECTURE.md, "SourceCode and Language objects";
68
+ * design/PACKAGES.md, `@eslint-sh/eslint-plugin`). Registered by the plugin
69
+ * as `languages.sh`, so consumers select it via `language: "sh/sh"`.
70
+ *
71
+ * Implements `matchesSelectorClass` (the six launch esquery selector
72
+ * classes — see `./selector-classes.js`), so rules and end users can write
73
+ * class-based selectors (`:command`, `:loop`, `:conditional`, `:expansion`,
74
+ * `:quoted`, `:function`) against shell trees. The `SourceCode` directive
75
+ * members (`getDisableDirectives`, `getInlineConfigNodes`,
76
+ * `applyInlineConfig`) are implemented on `ShellSourceCode` itself; see
77
+ * shell-source-code.ts.
78
+ *
79
+ * @public
80
+ */
81
+ export declare class ShellLanguage implements Language {
82
+ readonly fileType: "text";
83
+ readonly lineStart: 1;
84
+ readonly columnStart: 1;
85
+ readonly nodeTypeKey: string;
86
+ readonly visitorKeys: Record<string, string[]>;
87
+ readonly defaultLanguageOptions: ShellLanguageOptions;
88
+ /**
89
+ * Validates `languageOptions.dialect` (must be one of the five supported
90
+ * dialects, if present), `languageOptions.shellVersion` (must be a
91
+ * string matching the `MAJOR[.MINOR]` numeric shape, if present), and
92
+ * `languageOptions.directivePrefix` (must be a valid directive-comment
93
+ * label, if present). Unknown keys are silently ignored, per the
94
+ * `Language` contract. See `shell-language-options.ts`'s
95
+ * `validateShellLanguageOptions` for the full rationale.
96
+ */
97
+ validateLanguageOptions(languageOptions: ShellLanguageOptions): void;
98
+ /**
99
+ * Resolves `dialect` to its default (`"bash"`) when unset and attaches a
100
+ * `toJSON()` method with a fixed key order, so that ESLint's config
101
+ * caching (keyed off serializing `languageOptions`) is stable regardless
102
+ * of the key order a user's config happened to use. See
103
+ * `shell-language-options.ts`'s `normalizeShellLanguageOptions`.
104
+ */
105
+ normalizeLanguageOptions(languageOptions: ShellLanguageOptions): NormalizedShellLanguageOptions;
106
+ /**
107
+ * `@eslint/core`'s esquery hook: lets a selector's leading `:` "class"
108
+ * (e.g. `:command` in `CallExpr[args.0.parts.0.value='jq']` isn't even
109
+ * needed there, but `:loop :command` is) match a whole family of node
110
+ * types instead of one literal type name. Delegates to
111
+ * `./selector-classes.js`'s `SELECTOR_CLASS_NODE_TYPES` table for the six
112
+ * launch classes (design/ARCHITECTURE.md, "SourceCode and Language
113
+ * objects"); `ancestry` is unused today because none of the six launch
114
+ * classes need ancestor context, but the parameter is accepted to match
115
+ * `Language.matchesSelectorClass`'s signature exactly.
116
+ */
117
+ matchesSelectorClass(className: string, node: ShNode, _ancestry: ShNode[]): boolean;
118
+ /**
119
+ * Parses `file.body` (guaranteed to be a `string`, since `fileType` is
120
+ * `"text"`) via `sh-ast`'s `parseSync`, mapping the result to
121
+ * the `{ok: true, ast}` / `{ok: false, errors}` shape `Language.parse` is
122
+ * required to return. Never throws — parse failures are reported as
123
+ * fatal lint messages, per the `Language` contract
124
+ * (design/ARCHITECTURE.md, "Parse errors and dialects").
125
+ */
126
+ parse(file: File_2, context: {
127
+ languageOptions: ShellLanguageOptions;
128
+ }): ParseResult<ShFile>;
129
+ /**
130
+ * Creates the `ShellSourceCode` ESLint uses to run rules against `ast`.
131
+ * `context.languageOptions` is ESLint's already-normalized copy (see
132
+ * `packages/eslint-plugin/test/language-options.test.ts` and
133
+ * `eslint`'s `Config#finalize`, which replaces `languageOptions` with
134
+ * `normalizeLanguageOptions`'s return value before either `parse` or
135
+ * `createSourceCode` is called) — `directivePrefix` is therefore always
136
+ * present, but `?? 'eslint'` stays as an explicit, honest fallback for
137
+ * direct construction (e.g. tests that skip the `Language` normalization
138
+ * step), mirroring `parse()`'s `dialect` default above.
139
+ */
140
+ createSourceCode(file: File_2, parseResult: OkParseResult<ShFile>, context: {
141
+ languageOptions: ShellLanguageOptions;
142
+ }): ShellSourceCode;
143
+ }
144
+
145
+ /**
146
+ * `languageOptions` accepted by {@link ShellLanguage} (design/PACKAGES.md's
147
+ * `languageOptions` schema): `dialect` selects mvdan/sh's `LangVariant` by
148
+ * string name (default `"bash"`); `shellVersion` is consumed by the
149
+ * rule layer (e.g. the bash-3.2 compatibility pack), never by the parser
150
+ * itself (see design/ARCHITECTURE.md, "A verified nuance worth stating
151
+ * loudly").
152
+ *
153
+ * Extends `Record<string, unknown>` (rather than a bare interface) so this
154
+ * type satisfies the `@eslint/core` package's `LanguageOptions` bound
155
+ * (itself `Record<string, unknown>`) wherever it is used as a
156
+ * `Language`/`SourceCode` type parameter — ESLint's own contract for
157
+ * language options is that they are arbitrary user-supplied records, not a
158
+ * closed shape.
159
+ *
160
+ * @public
161
+ */
162
+ export declare interface ShellLanguageOptions extends Record<string, unknown> {
163
+ readonly dialect?: ShellDialect;
164
+ readonly shellVersion?: string;
165
+ /**
166
+ * The prefix directive/inline-config comments must start with — e.g. the
167
+ * default `"eslint"` recognizes `# eslint-disable-next-line ...` and
168
+ * `# eslint <rule-config>`. Consumers who embed shell-linting inside a
169
+ * larger toolchain that already overloads `#eslint-*` comments for
170
+ * something else can configure a different prefix (design/ARCHITECTURE.md,
171
+ * "SourceCode and Language objects").
172
+ * @defaultValue `"eslint"`
173
+ */
174
+ readonly directivePrefix?: string;
175
+ }
176
+
177
+ /**
178
+ * The plain-data shape {@link NormalizedShellLanguageOptions.toJSON} returns:
179
+ * `dialect` and `directivePrefix` always present (both defaulted), `shellVersion`
180
+ * present only when the caller supplied one. Keys are always emitted in this
181
+ * fixed order so that two logically-identical inputs serialize identically
182
+ * regardless of the key order the caller happened to use — the property this
183
+ * type exists to guarantee for cache-key stability (see
184
+ * {@link NormalizedShellLanguageOptions}). `directivePrefix` is included
185
+ * unconditionally (like `dialect`, unlike `shellVersion`) because — like
186
+ * `dialect` and unlike `shellVersion` — it always has a concrete effect on
187
+ * linting results (which comments the `SourceCode` recognizes as directives)
188
+ * whether or not the caller supplied one, so two configs that differ only in
189
+ * an *implicit* default vs. an *explicit* `directivePrefix: "eslint"` must
190
+ * still cache-key identically.
191
+ *
192
+ * @public
193
+ */
194
+ export declare interface ShellLanguageOptionsJSON {
195
+ readonly dialect: ShellDialect;
196
+ readonly directivePrefix: string;
197
+ readonly shellVersion?: string;
198
+ }
199
+
200
+ /**
201
+ * `ShellSourceCode` — the `SourceCode` object ESLint uses to work with a
202
+ * parsed shell file (design/ARCHITECTURE.md, "SourceCode and Language
203
+ * objects"). Extends `@eslint/plugin-kit`'s `TextSourceCodeBase`, which
204
+ * already implements `getLoc`/`getRange`/`getText`/`lines` (both by reading
205
+ * the normalized node's own `loc`/`range` fields — see
206
+ * `sh-ast`'s "Normalized node shape"), `getLocFromIndex`, and
207
+ * `getIndexFromLoc`; this class adds the two members the base class
208
+ * deliberately leaves unimplemented: `traverse()` (base throws "Not
209
+ * implemented") and `getParent()` (base throws "Not implemented", and
210
+ * `getAncestors()` — already implemented generically in terms of
211
+ * `getParent()` — falls out for free).
212
+ *
213
+ * Directive support (`getDisableDirectives`, `getInlineConfigNodes`,
214
+ * `applyInlineConfig`) recognizes all four disable-comment forms
215
+ * (`<prefix>-disable`, `<prefix>-enable`, `<prefix>-disable-line`,
216
+ * `<prefix>-disable-next-line`) plus whole-file inline rule config
217
+ * (bare `<prefix> <rule-config>`), sourced from `Comment` nodes gathered via
218
+ * `sh-ast`'s structural `walk()` and parsed with
219
+ * `@eslint/plugin-kit`'s `ConfigCommentParser` — the same building blocks
220
+ * `@eslint/json`'s `JSONSourceCode` uses, generalized so the `<prefix>` is
221
+ * configurable (default `"eslint"`; see `ShellLanguageOptions.directivePrefix`)
222
+ * rather than hardcoded to `"eslint"`.
223
+ *
224
+ * Selector-class support (`matchesSelectorClass`, the six launch esquery
225
+ * classes) is implemented on `ShellLanguage` — see `shell-language.ts` and
226
+ * `selector-classes.ts`.
227
+ *
228
+ * @public
229
+ */
230
+ export declare class ShellSourceCode extends TextSourceCodeBase<{
231
+ LangOptions: ShellLanguageOptions;
232
+ RootNode: ShFile;
233
+ SyntaxElementWithLoc: ShNode;
234
+ ConfigNode: ShNodes.ShCommentNode;
235
+ }> {
236
+ #private;
237
+ constructor(options: {
238
+ text: string;
239
+ ast: ShFile;
240
+ directivePrefix?: string;
241
+ });
242
+ /**
243
+ * Returns the parent of `node`, or `undefined` for the root `File` node
244
+ * (or any node visited before `traverse()` has run once). ESLint always
245
+ * calls `traverse()` before any rule can call `getParent`/`getAncestors`,
246
+ * so in practice the map is always populated by the time a rule observes
247
+ * it.
248
+ */
249
+ getParent(node: ShNode): ShNode | undefined;
250
+ /**
251
+ * Traverses the AST pre-order, yielding a `VisitNodeStep` on node enter
252
+ * (`phase: 1`) and another on node exit (`phase: 2`) for every node —
253
+ * exactly the two-phase shape `@eslint/json`/`@eslint/css` produce, which
254
+ * is what lets rule listeners use `NodeType` and `NodeType:exit` esquery
255
+ * selectors. Children are discovered via `sh-ast`'s generated,
256
+ * frozen `visitorKeys` table (per node `type`) — **not** structurally
257
+ * (contrast `sh-ast`'s own `walk()`, which deliberately walks
258
+ * structurally so its correctness never depends on that table) — because
259
+ * ESLint's traversal contract is defined in terms of a language's
260
+ * declared `visitorKeys`, and this class's whole job is to honor that
261
+ * contract for shell ASTs.
262
+ */
263
+ traverse(): Iterable<TraversalStep>;
264
+ /**
265
+ * Returns every `Comment` node whose text is a directive or inline-config
266
+ * candidate for this source code's `directivePrefix` — the pool both
267
+ * {@link ShellSourceCode.getDisableDirectives} and {@link ShellSourceCode.applyInlineConfig} draw from,
268
+ * and the array ESLint core itself asks for directly (to warn "has no
269
+ * effect" when `linterOptions.noInlineConfig` is set — see
270
+ * `eslint`'s `lib/linter/linter.js`, the `warnInlineConfig` branch).
271
+ */
272
+ getInlineConfigNodes(): ShNodes.ShCommentNode[];
273
+ /**
274
+ * Returns the disable/enable directives found among
275
+ * {@link ShellSourceCode.getInlineConfigNodes}'s comments, recognizing all four forms —
276
+ * `<prefix>-disable`, `<prefix>-enable`, `<prefix>-disable-line`,
277
+ * `<prefix>-disable-next-line` — with `-- justification` capture, per the
278
+ * `@eslint/core` `Directive` contract. Mirrors `@eslint/json`'s
279
+ * `JSONSourceCode.getDisableDirectives` (verified against its published
280
+ * source as the named ecosystem precedent), generalized to a configurable
281
+ * prefix instead of the hardcoded `"eslint"` label.
282
+ *
283
+ * Unlike `JSONSourceCode` (whose block comments can span multiple
284
+ * physical lines, making `<prefix>-disable-line` spanning lines a real,
285
+ * reportable problem), mvdan/sh's `#` line comments cannot contain an
286
+ * embedded newline — the parser always terminates a comment at end of
287
+ * line — so that particular problem can never arise here and is not
288
+ * checked for.
289
+ */
290
+ getDisableDirectives(): {
291
+ directives: Directive[];
292
+ problems: FileProblem[];
293
+ };
294
+ /**
295
+ * Returns the whole-file inline rule configuration found among
296
+ * {@link ShellSourceCode.getInlineConfigNodes}'s comments — a bare `<prefix> <rule-config>`
297
+ * comment (e.g. `# eslint sh/rule-a: "error"`), parsed with
298
+ * `ConfigCommentParser.parseJSONLikeConfig`. Mirrors `@eslint/json`'s
299
+ * `JSONSourceCode.applyInlineConfig`. This is the surface
300
+ * `linterOptions.noInlineConfig: true` suppresses (ESLint core skips
301
+ * calling this method — and `getDisableDirectives`, together, since both
302
+ * are gated by the same flag in `eslint`'s own `linter.js` — and instead
303
+ * calls `getInlineConfigNodes` to emit a "has no effect" warning per
304
+ * comment; see design/ARCHITECTURE.md, "SourceCode and Language objects").
305
+ */
306
+ applyInlineConfig(): {
307
+ configs: {
308
+ config: {
309
+ rules: RulesConfig;
310
+ };
311
+ loc: SourceLocation;
312
+ }[];
313
+ problems: FileProblem[];
314
+ };
315
+ }
316
+
317
+ export { }
@@ -0,0 +1,34 @@
1
+ import { ShellLanguage } from './language/shell-language.js';
2
+ export { ShellLanguage } from './language/shell-language.js';
3
+ export { ShellSourceCode } from './language/shell-source-code.js';
4
+ export type { NormalizedShellLanguageOptions, ShellLanguageOptions, ShellLanguageOptionsJSON, } from './language/shell-language-options.js';
5
+ /**
6
+ * The `@eslint-sh/eslint-plugin` plugin object (design/PACKAGES.md,
7
+ * `@eslint-sh/eslint-plugin`): `languages.sh` provides `ShellLanguage` /
8
+ * `ShellSourceCode` (design/ARCHITECTURE.md, "SourceCode and Language
9
+ * objects"), selected in a flat config via `language: "sh/sh"`
10
+ * (`namespace: "sh"`). `meta.version` is read from this package's own
11
+ * `package.json` at runtime — never hardcoded (see `version.ts`).
12
+ *
13
+ * `rules` is empty at this milestone (M2 — `ShellLanguage`/`ShellSourceCode`
14
+ * core, plus the six launch esquery selector classes); rules and shared
15
+ * `configs` are Milestone 3 (design/MILESTONES.md). Adding them later is
16
+ * additive, not a breaking change to this object's shape.
17
+ *
18
+ * Re-exported, unchanged, as the default export of the `eslint-sh` umbrella
19
+ * package (design/PACKAGES.md's Naming decision): importing this package or
20
+ * the umbrella package both yield the same plugin object.
21
+ */
22
+ declare const _default: {
23
+ meta: {
24
+ name: string;
25
+ version: string;
26
+ namespace: string;
27
+ };
28
+ languages: {
29
+ sh: ShellLanguage;
30
+ };
31
+ rules: {};
32
+ };
33
+ export default _default;
34
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAG7D,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,YAAY,EACV,8BAA8B,EAC9B,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,sCAAsC,CAAC;AAE9C;;;;;;;;;;;;;;;;GAgBG;;;;;;;;;;;;AACH,wBAUmB"}
package/dist/index.js ADDED
@@ -0,0 +1,33 @@
1
+ import { ShellLanguage } from './language/shell-language.js';
2
+ import { readPackageVersion } from './version.js';
3
+ export { ShellLanguage } from './language/shell-language.js';
4
+ export { ShellSourceCode } from './language/shell-source-code.js';
5
+ /**
6
+ * The `@eslint-sh/eslint-plugin` plugin object (design/PACKAGES.md,
7
+ * `@eslint-sh/eslint-plugin`): `languages.sh` provides `ShellLanguage` /
8
+ * `ShellSourceCode` (design/ARCHITECTURE.md, "SourceCode and Language
9
+ * objects"), selected in a flat config via `language: "sh/sh"`
10
+ * (`namespace: "sh"`). `meta.version` is read from this package's own
11
+ * `package.json` at runtime — never hardcoded (see `version.ts`).
12
+ *
13
+ * `rules` is empty at this milestone (M2 — `ShellLanguage`/`ShellSourceCode`
14
+ * core, plus the six launch esquery selector classes); rules and shared
15
+ * `configs` are Milestone 3 (design/MILESTONES.md). Adding them later is
16
+ * additive, not a breaking change to this object's shape.
17
+ *
18
+ * Re-exported, unchanged, as the default export of the `eslint-sh` umbrella
19
+ * package (design/PACKAGES.md's Naming decision): importing this package or
20
+ * the umbrella package both yield the same plugin object.
21
+ */
22
+ export default {
23
+ meta: {
24
+ name: '@eslint-sh/eslint-plugin',
25
+ version: readPackageVersion(),
26
+ namespace: 'sh',
27
+ },
28
+ languages: {
29
+ sh: new ShellLanguage(),
30
+ },
31
+ rules: {},
32
+ };
33
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAOlE;;;;;;;;;;;;;;;;GAgBG;AACH,eAAe;IACb,IAAI,EAAE;QACJ,IAAI,EAAE,0BAA0B;QAChC,OAAO,EAAE,kBAAkB,EAAE;QAC7B,SAAS,EAAE,IAAI;KAChB;IACD,SAAS,EAAE;QACT,EAAE,EAAE,IAAI,aAAa,EAAE;KACxB;IACD,KAAK,EAAE,EAAE;CACO,CAAC"}
@@ -0,0 +1,57 @@
1
+ import type { ShNode } from 'sh-ast';
2
+ /**
3
+ * The six launch esquery selector classes `ShellLanguage.matchesSelectorClass`
4
+ * implements (design/ARCHITECTURE.md, "SourceCode and Language objects";
5
+ * the issue that introduced this module). Keys are the *bare* class name —
6
+ * esquery strips the leading `:` before invoking a language's
7
+ * `matchesSelectorClass` (confirmed against `esquery`'s own `case 'class'`
8
+ * matcher, which calls `options.matchClass(selector.name, node, ancestry)`
9
+ * with `selector.name` already colon-free) — so a selector written as
10
+ * `:command` calls this table with `"command"`, not `":command"`.
11
+ *
12
+ * `:loop` intentionally matches only `ForClause` and `WhileClause` — the
13
+ * two loop *statement* nodes, exactly as the bridge's generated schema
14
+ * names them (`packages/bridge/generated/visitor-keys.js`), even though
15
+ * design/ARCHITECTURE.md's prose shorthand says "For/While". `CStyleLoop`
16
+ * is deliberately excluded from `:loop`: it is `ForClause.loop`'s nested
17
+ * C-style arithmetic header (`init`/`cond`/`post` — see
18
+ * `packages/bridge/generated/node-types.d.ts`'s `ShCStyleLoopNode`), not a
19
+ * loop statement in its own right. A `for ((i=0;i<3;i++))` loop's outer
20
+ * node is still `ForClause`, so `:loop` already reaches every C-style loop
21
+ * without needing to include `CStyleLoop` itself.
22
+ *
23
+ * `as const satisfies Record<string, readonly ShNodes.ShNodeTypeName[]>`
24
+ * pins every listed node type to a real member of the bridge's generated
25
+ * type union — a typo or a since-renamed node type here is a compile error,
26
+ * not a silent no-op match.
27
+ *
28
+ * @public
29
+ */
30
+ export declare const SELECTOR_CLASS_NODE_TYPES: {
31
+ readonly command: readonly ["CallExpr"];
32
+ readonly loop: readonly ["ForClause", "WhileClause"];
33
+ readonly conditional: readonly ["IfClause", "CaseClause"];
34
+ readonly expansion: readonly ["ParamExp", "CmdSubst", "ArithmExp", "ProcSubst"];
35
+ readonly quoted: readonly ["SglQuoted", "DblQuoted"];
36
+ readonly function: readonly ["FuncDecl"];
37
+ };
38
+ /**
39
+ * The six launch class names, bare (no leading `:`) — see
40
+ * {@link SELECTOR_CLASS_NODE_TYPES}.
41
+ *
42
+ * @public
43
+ */
44
+ export type SelectorClassName = keyof typeof SELECTOR_CLASS_NODE_TYPES;
45
+ /**
46
+ * Implements the matching behavior behind `ShellLanguage.matchesSelectorClass`
47
+ * (`@eslint/core`'s `Language.matchesSelectorClass`): `true` when `node`'s
48
+ * `type` belongs to the named launch class, `false` for every other node
49
+ * type (including the five other launch classes' node types) and for any
50
+ * unrecognized `className` (a class name this language does not define —
51
+ * per the `Language` contract, an unknown class simply never matches,
52
+ * rather than throwing).
53
+ *
54
+ * @public
55
+ */
56
+ export declare function matchesSelectorClass(className: string, node: ShNode): boolean;
57
+ //# sourceMappingURL=selector-classes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selector-classes.d.ts","sourceRoot":"","sources":["../../src/language/selector-classes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAW,MAAM,QAAQ,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,yBAAyB;;;;;;;CAOgC,CAAC;AAEvE;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,yBAAyB,CAAC;AASvE;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAE7E"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The six launch esquery selector classes `ShellLanguage.matchesSelectorClass`
3
+ * implements (design/ARCHITECTURE.md, "SourceCode and Language objects";
4
+ * the issue that introduced this module). Keys are the *bare* class name —
5
+ * esquery strips the leading `:` before invoking a language's
6
+ * `matchesSelectorClass` (confirmed against `esquery`'s own `case 'class'`
7
+ * matcher, which calls `options.matchClass(selector.name, node, ancestry)`
8
+ * with `selector.name` already colon-free) — so a selector written as
9
+ * `:command` calls this table with `"command"`, not `":command"`.
10
+ *
11
+ * `:loop` intentionally matches only `ForClause` and `WhileClause` — the
12
+ * two loop *statement* nodes, exactly as the bridge's generated schema
13
+ * names them (`packages/bridge/generated/visitor-keys.js`), even though
14
+ * design/ARCHITECTURE.md's prose shorthand says "For/While". `CStyleLoop`
15
+ * is deliberately excluded from `:loop`: it is `ForClause.loop`'s nested
16
+ * C-style arithmetic header (`init`/`cond`/`post` — see
17
+ * `packages/bridge/generated/node-types.d.ts`'s `ShCStyleLoopNode`), not a
18
+ * loop statement in its own right. A `for ((i=0;i<3;i++))` loop's outer
19
+ * node is still `ForClause`, so `:loop` already reaches every C-style loop
20
+ * without needing to include `CStyleLoop` itself.
21
+ *
22
+ * `as const satisfies Record<string, readonly ShNodes.ShNodeTypeName[]>`
23
+ * pins every listed node type to a real member of the bridge's generated
24
+ * type union — a typo or a since-renamed node type here is a compile error,
25
+ * not a silent no-op match.
26
+ *
27
+ * @public
28
+ */
29
+ export const SELECTOR_CLASS_NODE_TYPES = {
30
+ command: ['CallExpr'],
31
+ loop: ['ForClause', 'WhileClause'],
32
+ conditional: ['IfClause', 'CaseClause'],
33
+ expansion: ['ParamExp', 'CmdSubst', 'ArithmExp', 'ProcSubst'],
34
+ quoted: ['SglQuoted', 'DblQuoted'],
35
+ function: ['FuncDecl'],
36
+ };
37
+ const nodeTypesByClassName = new Map(Object.entries(SELECTOR_CLASS_NODE_TYPES).map(([className, nodeTypes]) => [
38
+ className,
39
+ new Set(nodeTypes),
40
+ ]));
41
+ /**
42
+ * Implements the matching behavior behind `ShellLanguage.matchesSelectorClass`
43
+ * (`@eslint/core`'s `Language.matchesSelectorClass`): `true` when `node`'s
44
+ * `type` belongs to the named launch class, `false` for every other node
45
+ * type (including the five other launch classes' node types) and for any
46
+ * unrecognized `className` (a class name this language does not define —
47
+ * per the `Language` contract, an unknown class simply never matches,
48
+ * rather than throwing).
49
+ *
50
+ * @public
51
+ */
52
+ export function matchesSelectorClass(className, node) {
53
+ return nodeTypesByClassName.get(className)?.has(node.type) ?? false;
54
+ }
55
+ //# sourceMappingURL=selector-classes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selector-classes.js","sourceRoot":"","sources":["../../src/language/selector-classes.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACvC,OAAO,EAAE,CAAC,UAAU,CAAC;IACrB,IAAI,EAAE,CAAC,WAAW,EAAE,aAAa,CAAC;IAClC,WAAW,EAAE,CAAC,UAAU,EAAE,YAAY,CAAC;IACvC,SAAS,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,CAAC;IAC7D,MAAM,EAAE,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,EAAE,CAAC,UAAU,CAAC;CAC8C,CAAC;AAUvE,MAAM,oBAAoB,GAA6C,IAAI,GAAG,CAC5E,MAAM,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,EAAE,CAAC;IACxE,SAAS;IACT,IAAI,GAAG,CAAS,SAAS,CAAC;CAC3B,CAAC,CACH,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAAC,SAAiB,EAAE,IAAY;IAClE,OAAO,oBAAoB,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC;AACtE,CAAC"}