@eslint-sh/eslint-plugin 0.4.0 → 0.6.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/README.md +223 -17
- package/dist/command-harvest/builtins/bash.d.ts +36 -0
- package/dist/command-harvest/builtins/bash.js +98 -0
- package/dist/command-harvest/builtins/bats.d.ts +45 -0
- package/dist/command-harvest/builtins/bats.js +33 -0
- package/dist/command-harvest/builtins/index.d.ts +18 -0
- package/dist/command-harvest/builtins/index.js +39 -0
- package/dist/command-harvest/builtins/posix.d.ts +51 -0
- package/dist/command-harvest/builtins/posix.js +93 -0
- package/dist/command-harvest/harvest-commands.d.ts +66 -0
- package/dist/command-harvest/harvest-commands.js +97 -0
- package/dist/command-harvest/wrapper-table.d.ts +24 -0
- package/dist/command-harvest/wrapper-table.js +49 -0
- package/dist/configs/agent-authored.d.ts +41 -0
- package/dist/configs/agent-authored.js +33 -0
- package/dist/configs/bash-3-2.d.ts +28 -0
- package/dist/configs/bash-3-2.js +50 -0
- package/dist/configs/posix-portable.d.ts +40 -0
- package/dist/configs/posix-portable.js +52 -0
- package/dist/configs/recommended.d.ts +52 -0
- package/dist/configs/recommended.js +34 -0
- package/dist/declaration-conventions/comment-pragma.d.ts +33 -0
- package/dist/declaration-conventions/comment-pragma.js +153 -0
- package/dist/declaration-conventions/declaration-convention.d.ts +189 -0
- package/dist/declaration-conventions/declaration-convention.js +1 -0
- package/dist/declaration-conventions/read-declared-commands.d.ts +70 -0
- package/dist/declaration-conventions/read-declared-commands.js +128 -0
- package/dist/eslint-plugin-syntax-analyze.d.ts +23 -0
- package/dist/eslint-plugin-syntax.d.ts +27 -0
- package/dist/eslint-plugin.d.ts +1211 -52
- package/dist/index.d.ts +56 -8
- package/dist/index.js +53 -6
- package/dist/language/command-resolution.d.ts +124 -0
- package/dist/language/command-resolution.js +120 -0
- package/dist/language/end-of-input-marker.d.ts +29 -0
- package/dist/language/end-of-input-marker.js +88 -0
- package/dist/language/line-breaks.d.ts +18 -0
- package/dist/language/line-breaks.js +22 -0
- package/dist/language/lint-view-types.d.ts +104 -0
- package/dist/language/lint-view-types.js +1 -0
- package/dist/language/non-shell-file-guard.d.ts +35 -0
- package/dist/language/non-shell-file-guard.js +59 -0
- package/dist/language/selector-classes.d.ts +0 -1
- package/dist/language/selector-classes.js +0 -1
- package/dist/language/shell-language-options.d.ts +7 -1
- package/dist/language/shell-language-options.js +4 -4
- package/dist/language/shell-language.d.ts +10 -2
- package/dist/language/shell-language.js +59 -8
- package/dist/language/shell-source-code.d.ts +342 -5
- package/dist/language/shell-source-code.js +559 -30
- package/dist/language/source-coordinates.d.ts +13 -2
- package/dist/language/source-coordinates.js +68 -14
- package/dist/language/syntax-view.d.ts +60 -39
- package/dist/language/syntax-view.js +200 -45
- package/dist/language/word-resolution.d.ts +4 -3
- package/dist/language/word-resolution.js +4 -3
- package/dist/rules/compat/create-compat-rule.d.ts +59 -0
- package/dist/rules/compat/create-compat-rule.js +65 -0
- package/dist/rules/compat/declaration-options.d.ts +19 -0
- package/dist/rules/compat/declaration-options.js +110 -0
- package/dist/rules/compat/no-append-all-redirect.d.ts +13 -0
- package/dist/rules/compat/no-append-all-redirect.js +73 -0
- package/dist/rules/compat/no-associative-arrays.d.ts +12 -0
- package/dist/rules/compat/no-associative-arrays.js +39 -0
- package/dist/rules/compat/no-case-attributes.d.ts +12 -0
- package/dist/rules/compat/no-case-attributes.js +63 -0
- package/dist/rules/compat/no-case-fallthrough.d.ts +12 -0
- package/dist/rules/compat/no-case-fallthrough.js +67 -0
- package/dist/rules/compat/no-case-modification.d.ts +35 -0
- package/dist/rules/compat/no-case-modification.js +86 -0
- package/dist/rules/compat/no-coproc.d.ts +12 -0
- package/dist/rules/compat/no-coproc.js +47 -0
- package/dist/rules/compat/no-declare-g.d.ts +12 -0
- package/dist/rules/compat/no-declare-g.js +59 -0
- package/dist/rules/compat/no-double-bracket.d.ts +12 -0
- package/dist/rules/compat/no-double-bracket.js +52 -0
- package/dist/rules/compat/no-epoch-variables.d.ts +12 -0
- package/dist/rules/compat/no-epoch-variables.js +57 -0
- package/dist/rules/compat/no-fd-variable.d.ts +12 -0
- package/dist/rules/compat/no-fd-variable.js +48 -0
- package/dist/rules/compat/no-globstar.d.ts +12 -0
- package/dist/rules/compat/no-globstar.js +39 -0
- package/dist/rules/compat/no-lastpipe-semantics.d.ts +12 -0
- package/dist/rules/compat/no-lastpipe-semantics.js +53 -0
- package/dist/rules/compat/no-let.d.ts +12 -0
- package/dist/rules/compat/no-let.js +62 -0
- package/dist/rules/compat/no-mapfile.d.ts +13 -0
- package/dist/rules/compat/no-mapfile.js +122 -0
- package/dist/rules/compat/no-nameref.d.ts +12 -0
- package/dist/rules/compat/no-nameref.js +44 -0
- package/dist/rules/compat/no-negative-array-index.d.ts +13 -0
- package/dist/rules/compat/no-negative-array-index.js +123 -0
- package/dist/rules/compat/no-nounset-empty-array.d.ts +13 -0
- package/dist/rules/compat/no-nounset-empty-array.js +158 -0
- package/dist/rules/compat/no-parameter-transformation.d.ts +13 -0
- package/dist/rules/compat/no-parameter-transformation.js +123 -0
- package/dist/rules/compat/no-pipe-all.d.ts +13 -0
- package/dist/rules/compat/no-pipe-all.js +68 -0
- package/dist/rules/compat/no-printf-time-format.d.ts +12 -0
- package/dist/rules/compat/no-printf-time-format.js +99 -0
- package/dist/rules/compat/no-source.d.ts +13 -0
- package/dist/rules/compat/no-source.js +103 -0
- package/dist/rules/compat/no-test-v.d.ts +12 -0
- package/dist/rules/compat/no-test-v.js +280 -0
- package/dist/rules/compat/no-unicode-escape.d.ts +14 -0
- package/dist/rules/compat/no-unicode-escape.js +120 -0
- package/dist/rules/compat/no-wait-n.d.ts +12 -0
- package/dist/rules/compat/no-wait-n.js +73 -0
- package/dist/rules/compat/operator-range.d.ts +10 -0
- package/dist/rules/compat/operator-range.js +15 -0
- package/dist/rules/compat/posix-absent-command.d.ts +12 -0
- package/dist/rules/compat/posix-absent-command.js +134 -0
- package/dist/rules/compat/shopt-options.d.ts +9 -0
- package/dist/rules/compat/shopt-options.js +63 -0
- package/dist/rules/compat/version-gates.d.ts +76 -0
- package/dist/rules/compat/version-gates.js +197 -0
- package/dist/rules/create-rule.d.ts +91 -0
- package/dist/rules/create-rule.js +51 -0
- package/dist/rules/create-wrapper-rule.d.ts +166 -0
- package/dist/rules/create-wrapper-rule.js +407 -0
- package/dist/rules/define-shell-rule.d.ts +184 -0
- package/dist/rules/define-shell-rule.js +56 -0
- package/dist/rules/index.d.ts +26 -0
- package/dist/rules/index.js +88 -0
- package/dist/rules/max-script-complexity.d.ts +28 -0
- package/dist/rules/max-script-complexity.js +130 -0
- package/dist/rules/no-dynamic-invocation.d.ts +35 -0
- package/dist/rules/no-dynamic-invocation.js +117 -0
- package/dist/rules/no-eval.d.ts +13 -0
- package/dist/rules/no-eval.js +89 -0
- package/dist/rules/no-masked-exit-status.d.ts +21 -0
- package/dist/rules/no-masked-exit-status.js +667 -0
- package/dist/rules/no-secret-env-reads.d.ts +24 -0
- package/dist/rules/no-secret-env-reads.js +133 -0
- package/dist/rules/no-secret-flags.d.ts +32 -0
- package/dist/rules/no-secret-flags.js +162 -0
- package/dist/rules/no-unchecked-cd.d.ts +44 -0
- package/dist/rules/no-unchecked-cd.js +250 -0
- package/dist/rules/no-undeclared-commands.d.ts +20 -0
- package/dist/rules/no-undeclared-commands.js +115 -0
- package/dist/rules/no-unused-declarations.d.ts +23 -0
- package/dist/rules/no-unused-declarations.js +259 -0
- package/dist/rules/prefer-local.d.ts +16 -0
- package/dist/rules/prefer-local.js +249 -0
- package/dist/rules/quote-expansions.d.ts +13 -0
- package/dist/rules/quote-expansions.js +229 -0
- package/dist/rules/require-network-wrapper.d.ts +40 -0
- package/dist/rules/require-network-wrapper.js +244 -0
- package/dist/rules/require-shebang.d.ts +29 -0
- package/dist/rules/require-shebang.js +101 -0
- package/dist/rules/require-strict-mode.d.ts +52 -0
- package/dist/rules/require-strict-mode.js +177 -0
- package/dist/rules/require-tty-guard.d.ts +13 -0
- package/dist/rules/require-tty-guard.js +289 -0
- package/dist/rules/resolve-declaration-convention.d.ts +96 -0
- package/dist/rules/resolve-declaration-convention.js +109 -0
- package/dist/rules/resolve-network-wrapper.d.ts +19 -0
- package/dist/rules/resolve-network-wrapper.js +116 -0
- package/dist/rules/set-options.d.ts +109 -0
- package/dist/rules/set-options.js +201 -0
- package/dist/rules/shebang-interpreter.d.ts +18 -0
- package/dist/rules/shebang-interpreter.js +81 -0
- package/dist/rules/shebang-matches-dialect.d.ts +12 -0
- package/dist/rules/shebang-matches-dialect.js +69 -0
- package/dist/rules/transparent-wrappers.d.ts +52 -0
- package/dist/rules/transparent-wrappers.js +99 -0
- package/dist/rules/user-pattern.d.ts +63 -0
- package/dist/rules/user-pattern.js +184 -0
- package/dist/syntax/analyze.d.ts +19 -0
- package/dist/syntax/analyze.js +21 -0
- package/dist/syntax/index.d.ts +23 -0
- package/dist/syntax/index.js +25 -0
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -1
- package/docs/compat.md +347 -0
- package/docs/custom-rule-tutorial.md +780 -34
- package/docs/declaration-conventions.md +478 -0
- package/docs/examples/audit-wrapper-pack.mjs +15 -0
- package/docs/examples/dep-line-convention.mjs +22 -0
- package/docs/examples/eslint.config.mjs +3 -2
- package/docs/examples/no-and-or-as-if-rule.mjs +61 -0
- package/docs/examples/no-backticks-rule-tester.mjs +106 -0
- package/docs/examples/no-backticks-rule.mjs +60 -0
- package/docs/examples/no-curl-in-functions-rule.mjs +53 -0
- package/docs/examples/no-function-keyword-rule.mjs +59 -0
- package/docs/examples/no-listed-commands-rule.ts +63 -0
- package/docs/examples/no-rm-rf-rule.mjs +26 -15
- package/docs/examples/no-unquoted-rm-variable-rule.ts +54 -0
- package/docs/examples/sidecar-convention.mjs +38 -0
- package/docs/examples/sidecar-snapshot-convention.mjs +26 -0
- package/docs/examples/sorted-declarations-rule.mjs +52 -0
- package/docs/reading-nodes.md +159 -0
- package/docs/rules/README.md +78 -0
- package/docs/rules/_template.md +83 -0
- package/docs/rules/compat/no-append-all-redirect.md +87 -0
- package/docs/rules/compat/no-associative-arrays.md +95 -0
- package/docs/rules/compat/no-case-attributes.md +82 -0
- package/docs/rules/compat/no-case-fallthrough.md +90 -0
- package/docs/rules/compat/no-case-modification.md +74 -0
- package/docs/rules/compat/no-coproc.md +76 -0
- package/docs/rules/compat/no-declare-g.md +80 -0
- package/docs/rules/compat/no-double-bracket.md +72 -0
- package/docs/rules/compat/no-epoch-variables.md +79 -0
- package/docs/rules/compat/no-fd-variable.md +78 -0
- package/docs/rules/compat/no-globstar.md +74 -0
- package/docs/rules/compat/no-lastpipe-semantics.md +73 -0
- package/docs/rules/compat/no-let.md +78 -0
- package/docs/rules/compat/no-mapfile.md +118 -0
- package/docs/rules/compat/no-nameref.md +81 -0
- package/docs/rules/compat/no-negative-array-index.md +87 -0
- package/docs/rules/compat/no-nounset-empty-array.md +103 -0
- package/docs/rules/compat/no-parameter-transformation.md +94 -0
- package/docs/rules/compat/no-pipe-all.md +85 -0
- package/docs/rules/compat/no-printf-time-format.md +75 -0
- package/docs/rules/compat/no-source.md +98 -0
- package/docs/rules/compat/no-test-v.md +82 -0
- package/docs/rules/compat/no-unicode-escape.md +91 -0
- package/docs/rules/compat/no-wait-n.md +85 -0
- package/docs/rules/max-script-complexity.md +109 -0
- package/docs/rules/no-dynamic-invocation.md +139 -0
- package/docs/rules/no-eval.md +106 -0
- package/docs/rules/no-masked-exit-status.md +239 -0
- package/docs/rules/no-secret-env-reads.md +173 -0
- package/docs/rules/no-secret-flags.md +169 -0
- package/docs/rules/no-unchecked-cd.md +146 -0
- package/docs/rules/no-undeclared-commands.md +141 -0
- package/docs/rules/no-unused-declarations.md +216 -0
- package/docs/rules/prefer-local.md +221 -0
- package/docs/rules/quote-expansions.md +157 -0
- package/docs/rules/require-network-wrapper.md +171 -0
- package/docs/rules/require-shebang.md +157 -0
- package/docs/rules/require-strict-mode.md +253 -0
- package/docs/rules/require-tty-guard.md +128 -0
- package/docs/rules/shebang-matches-dialect.md +120 -0
- package/docs/selectors.md +2 -2
- package/docs/shellcheck.md +86 -0
- package/docs/wrapper-rules.md +206 -0
- package/package.json +26 -20
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/language/selector-classes.d.ts.map +0 -1
- package/dist/language/selector-classes.js.map +0 -1
- package/dist/language/shell-language-options.d.ts.map +0 -1
- package/dist/language/shell-language-options.js.map +0 -1
- package/dist/language/shell-language.d.ts.map +0 -1
- package/dist/language/shell-language.js.map +0 -1
- package/dist/language/shell-source-code.d.ts.map +0 -1
- package/dist/language/shell-source-code.js.map +0 -1
- package/dist/language/source-coordinates.d.ts.map +0 -1
- package/dist/language/source-coordinates.js.map +0 -1
- package/dist/language/syntax-view.d.ts.map +0 -1
- package/dist/language/syntax-view.js.map +0 -1
- package/dist/language/word-resolution.d.ts.map +0 -1
- package/dist/language/word-resolution.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,25 +1,68 @@
|
|
|
1
|
+
/** Exposes the ESLint language, source view, and rule-author helpers as one plugin boundary. */
|
|
2
|
+
import type { ConfigObject } from '@eslint/core';
|
|
1
3
|
import { ShellLanguage } from './language/shell-language.js';
|
|
2
4
|
export { ShellLanguage } from './language/shell-language.js';
|
|
3
5
|
export { ShellSourceCode } from './language/shell-source-code.js';
|
|
4
6
|
export type { NormalizedShellLanguageOptions, ShellLanguageOptions, ShellLanguageOptionsJSON, } from './language/shell-language-options.js';
|
|
5
7
|
export { resolveWord } from './language/word-resolution.js';
|
|
6
|
-
export type {
|
|
8
|
+
export type { Argv0Resolution } from 'sh-ast/analyze';
|
|
9
|
+
export type { CommandContext, ResolveArgv0Options, WordResolution } from 'sh-ast/analyze';
|
|
10
|
+
export type { ShellCommandSite } from './language/command-resolution.js';
|
|
11
|
+
export type { ShellComment, ShellFile, ShellNode, ShellNodeField, ShellNodeMap, ShellNodeOf, ShellNodeType, ShellPosition, } from './language/syntax-view.js';
|
|
7
12
|
export type { ShellDialect } from './language/shell-language-options.js';
|
|
13
|
+
export type { DeclarationConvention, DeclaredCommand, FileDeclaredCommand, LintedFile, DescribedDeclaredCommand, ScriptDeclaredCommand, } from './declaration-conventions/declaration-convention.js';
|
|
14
|
+
export { readDeclaredCommands } from './declaration-conventions/read-declared-commands.js';
|
|
15
|
+
export type { DeclarationReadContext } from './declaration-conventions/read-declared-commands.js';
|
|
16
|
+
export { commentPragma } from './declaration-conventions/comment-pragma.js';
|
|
17
|
+
export { resolveDeclarationConvention } from './rules/resolve-declaration-convention.js';
|
|
18
|
+
export type { DeclarationConventionBindingContext } from './rules/resolve-declaration-convention.js';
|
|
19
|
+
export { defineShellRule } from './rules/define-shell-rule.js';
|
|
20
|
+
export type { ShellListener, ShellRule, ShellRuleContext, ShellRuleMeta, ShellRuleSpec, ShellRuleVisitor, ShellTypedListeners, } from './rules/define-shell-rule.js';
|
|
21
|
+
export { createWrapperRule } from './rules/create-wrapper-rule.js';
|
|
22
|
+
export type { WrapperRuleConfig, WrapperRuleLocalInvocation, WrapperRuleLocalOperand, WrapperRuleSubcommands, } from './rules/create-wrapper-rule.js';
|
|
23
|
+
export type { WrapperSpec } from 'sh-ast/analyze';
|
|
8
24
|
/**
|
|
9
|
-
* The `@eslint-sh/eslint-plugin` plugin object
|
|
25
|
+
* The `@eslint-sh/eslint-plugin` plugin object, this package's default
|
|
26
|
+
* export (`import sh from '@eslint-sh/eslint-plugin'`). `languages.sh` provides
|
|
10
27
|
* `ShellLanguage` / `ShellSourceCode`, selected in a flat config via
|
|
11
28
|
* `language: "sh/sh"` (`namespace: "sh"`). `meta.version` is read from this
|
|
12
29
|
* package's own `package.json` at runtime — never hardcoded (see
|
|
13
30
|
* `version.ts`).
|
|
14
31
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
32
|
+
* `rules` holds the plugin's built-in rules, each built with
|
|
33
|
+
* `defineShellRule` (so each declares `meta.languages: ["sh/sh"]`) and
|
|
34
|
+
* linking its docs page from `meta.docs.url`. Consumers can also supply their
|
|
35
|
+
* own rules, built with the same `defineShellRule`, while sharing parsing,
|
|
36
|
+
* selectors, source coordinates, and directive semantics.
|
|
37
|
+
*
|
|
38
|
+
* `configs.recommended` is the `sh/recommended` preset: a single config
|
|
39
|
+
* object carrying `plugins: { sh }` and `language: "sh/sh"` but no `files`,
|
|
40
|
+
* so it inherits the globs of the entry that extends it:
|
|
41
|
+
* `{ files: [...], extends: [sh.configs.recommended] }`. It enables the
|
|
42
|
+
* convention-free policy rules. The rules index at
|
|
43
|
+
* https://github.com/mike-north/eslint-sh/blob/main/packages/eslint-plugin/docs/rules/README.md
|
|
44
|
+
* describes preset membership and available autofixes.
|
|
45
|
+
* `configs['agent-authored']` is the strict `sh/agent-authored` preset for
|
|
46
|
+
* machine-written, human-reviewed scripts:
|
|
47
|
+
* every `recommended` rule plus the contract rules, all at `error`.
|
|
48
|
+
* `configs['bash-3.2']` and `configs['posix-portable']` are deployment-target
|
|
49
|
+
* packs: the first sets `languageOptions.shellVersion: "3.2"` and enables
|
|
50
|
+
* every version-gated `compat/*` rule; the second sets
|
|
51
|
+
* `languageOptions.dialect: "posix"`, enables the `compat/*` rules and the
|
|
52
|
+
* shebang/dialect check, and requires `set -eu`; it reports named constructs
|
|
53
|
+
* that fail on dash, not every non-POSIX construct, and leaves `local`,
|
|
54
|
+
* `echo -e` and the rest of shellcheck's SC3xxx catalog to shellcheck. Every
|
|
55
|
+
* shipped config is a
|
|
56
|
+
* single object with no `files`; the two packs also carry `languageOptions`,
|
|
57
|
+
* because the target they name is their meaning.
|
|
17
58
|
*
|
|
18
59
|
* Re-exported, unchanged, as the default export of the `eslint-sh` umbrella
|
|
19
60
|
* package: importing this package or the umbrella package both yield the
|
|
20
61
|
* same plugin object.
|
|
62
|
+
*
|
|
63
|
+
* @public
|
|
21
64
|
*/
|
|
22
|
-
declare const
|
|
65
|
+
declare const plugin: {
|
|
23
66
|
meta: {
|
|
24
67
|
name: string;
|
|
25
68
|
version: string;
|
|
@@ -28,7 +71,12 @@ declare const _default: {
|
|
|
28
71
|
languages: {
|
|
29
72
|
sh: ShellLanguage;
|
|
30
73
|
};
|
|
31
|
-
rules:
|
|
74
|
+
rules: Record<string, import("@eslint/core").RuleDefinition<import("@eslint/core").RuleDefinitionTypeOptions>>;
|
|
75
|
+
configs: {
|
|
76
|
+
recommended: ConfigObject;
|
|
77
|
+
"agent-authored": ConfigObject;
|
|
78
|
+
"bash-3.2": ConfigObject;
|
|
79
|
+
"posix-portable": ConfigObject;
|
|
80
|
+
};
|
|
32
81
|
};
|
|
33
|
-
export default
|
|
34
|
-
//# sourceMappingURL=index.d.ts.map
|
|
82
|
+
export default plugin;
|
package/dist/index.js
CHANGED
|
@@ -1,23 +1,62 @@
|
|
|
1
|
+
import { createAgentAuthoredConfig } from './configs/agent-authored.js';
|
|
2
|
+
import { createBash32Config } from './configs/bash-3-2.js';
|
|
3
|
+
import { createPosixPortableConfig } from './configs/posix-portable.js';
|
|
4
|
+
import { createRecommendedConfig } from './configs/recommended.js';
|
|
1
5
|
import { ShellLanguage } from './language/shell-language.js';
|
|
6
|
+
import { rules } from './rules/index.js';
|
|
2
7
|
import { readPackageVersion } from './version.js';
|
|
3
8
|
export { ShellLanguage } from './language/shell-language.js';
|
|
4
9
|
export { ShellSourceCode } from './language/shell-source-code.js';
|
|
5
10
|
export { resolveWord } from './language/word-resolution.js';
|
|
11
|
+
export { readDeclaredCommands } from './declaration-conventions/read-declared-commands.js';
|
|
12
|
+
export { commentPragma } from './declaration-conventions/comment-pragma.js';
|
|
13
|
+
export { resolveDeclarationConvention } from './rules/resolve-declaration-convention.js';
|
|
14
|
+
export { defineShellRule } from './rules/define-shell-rule.js';
|
|
15
|
+
export { createWrapperRule } from './rules/create-wrapper-rule.js';
|
|
16
|
+
/* eslint-disable @api-extractor-tools/public-on-non-exported -- `plugin` is exported below as
|
|
17
|
+
`export default plugin`; the rule only recognizes inline exports. */
|
|
6
18
|
/**
|
|
7
|
-
* The `@eslint-sh/eslint-plugin` plugin object
|
|
19
|
+
* The `@eslint-sh/eslint-plugin` plugin object, this package's default
|
|
20
|
+
* export (`import sh from '@eslint-sh/eslint-plugin'`). `languages.sh` provides
|
|
8
21
|
* `ShellLanguage` / `ShellSourceCode`, selected in a flat config via
|
|
9
22
|
* `language: "sh/sh"` (`namespace: "sh"`). `meta.version` is read from this
|
|
10
23
|
* package's own `package.json` at runtime — never hardcoded (see
|
|
11
24
|
* `version.ts`).
|
|
12
25
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
26
|
+
* `rules` holds the plugin's built-in rules, each built with
|
|
27
|
+
* `defineShellRule` (so each declares `meta.languages: ["sh/sh"]`) and
|
|
28
|
+
* linking its docs page from `meta.docs.url`. Consumers can also supply their
|
|
29
|
+
* own rules, built with the same `defineShellRule`, while sharing parsing,
|
|
30
|
+
* selectors, source coordinates, and directive semantics.
|
|
31
|
+
*
|
|
32
|
+
* `configs.recommended` is the `sh/recommended` preset: a single config
|
|
33
|
+
* object carrying `plugins: { sh }` and `language: "sh/sh"` but no `files`,
|
|
34
|
+
* so it inherits the globs of the entry that extends it:
|
|
35
|
+
* `{ files: [...], extends: [sh.configs.recommended] }`. It enables the
|
|
36
|
+
* convention-free policy rules. The rules index at
|
|
37
|
+
* https://github.com/mike-north/eslint-sh/blob/main/packages/eslint-plugin/docs/rules/README.md
|
|
38
|
+
* describes preset membership and available autofixes.
|
|
39
|
+
* `configs['agent-authored']` is the strict `sh/agent-authored` preset for
|
|
40
|
+
* machine-written, human-reviewed scripts:
|
|
41
|
+
* every `recommended` rule plus the contract rules, all at `error`.
|
|
42
|
+
* `configs['bash-3.2']` and `configs['posix-portable']` are deployment-target
|
|
43
|
+
* packs: the first sets `languageOptions.shellVersion: "3.2"` and enables
|
|
44
|
+
* every version-gated `compat/*` rule; the second sets
|
|
45
|
+
* `languageOptions.dialect: "posix"`, enables the `compat/*` rules and the
|
|
46
|
+
* shebang/dialect check, and requires `set -eu`; it reports named constructs
|
|
47
|
+
* that fail on dash, not every non-POSIX construct, and leaves `local`,
|
|
48
|
+
* `echo -e` and the rest of shellcheck's SC3xxx catalog to shellcheck. Every
|
|
49
|
+
* shipped config is a
|
|
50
|
+
* single object with no `files`; the two packs also carry `languageOptions`,
|
|
51
|
+
* because the target they name is their meaning.
|
|
15
52
|
*
|
|
16
53
|
* Re-exported, unchanged, as the default export of the `eslint-sh` umbrella
|
|
17
54
|
* package: importing this package or the umbrella package both yield the
|
|
18
55
|
* same plugin object.
|
|
56
|
+
*
|
|
57
|
+
* @public
|
|
19
58
|
*/
|
|
20
|
-
|
|
59
|
+
const plugin = {
|
|
21
60
|
meta: {
|
|
22
61
|
name: '@eslint-sh/eslint-plugin',
|
|
23
62
|
version: readPackageVersion(),
|
|
@@ -26,6 +65,14 @@ export default {
|
|
|
26
65
|
languages: {
|
|
27
66
|
sh: new ShellLanguage(),
|
|
28
67
|
},
|
|
29
|
-
rules
|
|
68
|
+
rules,
|
|
69
|
+
// Filled in below: each shipped config registers this very object under
|
|
70
|
+
// `plugins.sh`, so the plugin must exist before its configs can.
|
|
71
|
+
configs: {},
|
|
30
72
|
};
|
|
31
|
-
|
|
73
|
+
/* eslint-enable @api-extractor-tools/public-on-non-exported */
|
|
74
|
+
plugin.configs.recommended = createRecommendedConfig(plugin);
|
|
75
|
+
plugin.configs['agent-authored'] = createAgentAuthoredConfig(plugin);
|
|
76
|
+
plugin.configs['bash-3.2'] = createBash32Config(plugin);
|
|
77
|
+
plugin.configs['posix-portable'] = createPosixPortableConfig(plugin);
|
|
78
|
+
export default plugin;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Argv0Resolution, CallExpr, CommandContext, CommandSite, ResolveArgv0Options, WordResolution } from 'sh-ast/analyze';
|
|
2
|
+
import type { ShellFile, ShellNode, ShellNodeOf } from './syntax-view.js';
|
|
3
|
+
/**
|
|
4
|
+
* Every command site in one parsed file, from a single `enumerateCommands`
|
|
5
|
+
* call. `sites` keeps analysis's own list and order (source order); `byNode`
|
|
6
|
+
* looks a site up by the raw `CallExpr` object analysis reports as
|
|
7
|
+
* `CommandSite.node`. A `CallExpr` with no entry invokes no command (an
|
|
8
|
+
* assignment-only statement such as `FOO=1`).
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export interface CommandSiteIndex {
|
|
12
|
+
readonly sites: readonly CommandSite[];
|
|
13
|
+
readonly byNode: ReadonlyMap<CallExpr, CommandSite>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* One command invocation in a linted shell file, in lint-view terms: an
|
|
17
|
+
* `sh-ast/analyze` `CommandSite` whose `node` is the `CallExpr` view node
|
|
18
|
+
* ESLint passes to rule visitors, so a rule can `context.report()` on it.
|
|
19
|
+
*
|
|
20
|
+
* The data comes from `sh-ast/analyze`'s `enumerateCommands` and is facts
|
|
21
|
+
* only: how the command is reached and what its words resolve to, never a
|
|
22
|
+
* verdict on whether running it is safe. A dynamic (`static: false`) `argv0`
|
|
23
|
+
* is a normal result.
|
|
24
|
+
*
|
|
25
|
+
* Read one through `ShellSourceCode.getCommandSites()`.
|
|
26
|
+
*
|
|
27
|
+
* Every rule linting a file sees the same sites, so each one is deeply
|
|
28
|
+
* frozen: the entry, its `context` array and every frame in it, its `argv`
|
|
29
|
+
* array, and every `WordResolution` in `argv` (`argv0` included). Writing to
|
|
30
|
+
* any of them throws in strict-mode code and is ignored otherwise, so one rule
|
|
31
|
+
* cannot change what another rule reads. The `node` is the shared lint view
|
|
32
|
+
* node, which, like every node ESLint passes to rules, is not frozen and must
|
|
33
|
+
* not be modified.
|
|
34
|
+
*
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
export interface ShellCommandSite {
|
|
38
|
+
/**
|
|
39
|
+
* The `CallExpr` view node for this command: the same object (`===`)
|
|
40
|
+
* ESLint passes to a `CallExpr` visitor, with `range` and `loc` in this
|
|
41
|
+
* file's UTF-16 coordinates.
|
|
42
|
+
*/
|
|
43
|
+
readonly node: ShellNodeOf<'CallExpr'>;
|
|
44
|
+
/**
|
|
45
|
+
* The path from the file root to this command, outermost frame first,
|
|
46
|
+
* exactly as `sh-ast/analyze` reports it. For example, `date` in
|
|
47
|
+
* `f() { echo "$(date)"; }` has
|
|
48
|
+
* `[{ kind: 'function', name: 'f' }, { kind: 'cmdSubst' }]`.
|
|
49
|
+
*/
|
|
50
|
+
readonly context: readonly CommandContext[];
|
|
51
|
+
/** The command word (`argv[0]`) resolved with `{ context: 'command-argument' }`. */
|
|
52
|
+
readonly argv0: WordResolution;
|
|
53
|
+
/**
|
|
54
|
+
* Every word of the command, `argv0` included, in argument order, each
|
|
55
|
+
* resolved with `{ context: 'command-argument' }`. Leading `VAR=val`
|
|
56
|
+
* assignments are not part of argv.
|
|
57
|
+
*/
|
|
58
|
+
readonly argv: readonly WordResolution[];
|
|
59
|
+
/**
|
|
60
|
+
* Follows this command's argv0 through transparent wrappers (`env`, `sudo`,
|
|
61
|
+
* `nohup`, `exec`, …) and returns `sh-ast/analyze`'s `resolveArgv0`
|
|
62
|
+
* result. Same answer as
|
|
63
|
+
* `ShellSourceCode.resolveInvokedCommand(node, options)`; both read the
|
|
64
|
+
* same per-file index.
|
|
65
|
+
*
|
|
66
|
+
* @param options - Forwarded to `resolveArgv0` unchanged.
|
|
67
|
+
* `transparentWrappers` replaces `sh-ast/analyze`'s
|
|
68
|
+
* `DEFAULT_TRANSPARENT_WRAPPERS`; spread the defaults into the array to
|
|
69
|
+
* extend them instead.
|
|
70
|
+
* @returns A new result on every call. Its `chain` and `chainIndices`
|
|
71
|
+
* arrays belong to the caller. `chainIndices` gives each chain entry's
|
|
72
|
+
* position in this site's `argv`.
|
|
73
|
+
* @throws `ShAnalyzeInvalidWrapperSpecError` (from `sh-ast/analyze`) when
|
|
74
|
+
* `options.transparentWrappers` contains a malformed entry.
|
|
75
|
+
*/
|
|
76
|
+
resolveInvokedCommand(options?: ResolveArgv0Options): Argv0Resolution;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Builds the {@link CommandSiteIndex} for a whole parsed file with a single
|
|
80
|
+
* `enumerateCommands` call over the file's original syntax. Each site's
|
|
81
|
+
* `node` is the parser's own `CallExpr` object, the same object the lint view
|
|
82
|
+
* associates with its projected `CallExpr`, so lookups are by identity.
|
|
83
|
+
*
|
|
84
|
+
* Building a `CommandSite` by hand instead would duplicate analysis's choices
|
|
85
|
+
* about which words form argv and how they resolve, which is exactly the drift
|
|
86
|
+
* this bridge exists to prevent.
|
|
87
|
+
*
|
|
88
|
+
* The index is shared by every rule linting the file, and its data reaches
|
|
89
|
+
* rules: `getCommandSites` exposes each site's `context`, `argv0` and `argv`.
|
|
90
|
+
* Analysis also shares one `context` array and its frames between
|
|
91
|
+
* sibling commands. So every site is frozen in place here, before any rule
|
|
92
|
+
* can see it (see {@link freezeCommandSite}).
|
|
93
|
+
*
|
|
94
|
+
* @throws TypeError when `file` has no associated parsed syntax.
|
|
95
|
+
* @throws `ShAnalyzeMaxDepthError` (from `sh-ast/analyze`) when the file's
|
|
96
|
+
* nesting exceeds analysis's recursion guard. It propagates unchanged: a
|
|
97
|
+
* partial index would silently omit real commands, so callers fail closed.
|
|
98
|
+
* @internal
|
|
99
|
+
*/
|
|
100
|
+
export declare function buildCommandSiteIndex(file: ShellFile): CommandSiteIndex;
|
|
101
|
+
/**
|
|
102
|
+
* Re-expresses every site in `index` in lint-view terms, in analysis's order.
|
|
103
|
+
* Each `node` comes from the syntax → view association recorded when `file`
|
|
104
|
+
* was projected, so no second walk of the view is needed and the node is the
|
|
105
|
+
* very object ESLint's traversal visits. The returned array and its entries
|
|
106
|
+
* are frozen, and the `context`, `argv0` and `argv` they carry were frozen
|
|
107
|
+
* when the index was built: the same array is shared by every rule linting
|
|
108
|
+
* this file.
|
|
109
|
+
*
|
|
110
|
+
* @throws Error when a site's `CallExpr` has no view node in `file`. Analysis
|
|
111
|
+
* of the same parse only reports parser nodes the projection saw, so this
|
|
112
|
+
* marks a broken invariant rather than a shell construct; failing loudly is
|
|
113
|
+
* safer than silently dropping a command.
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
export declare function viewCommandSites(file: ShellFile, index: CommandSiteIndex): readonly ShellCommandSite[];
|
|
117
|
+
/**
|
|
118
|
+
* Returns the parser's own `CallExpr` behind a lint `CallExpr`.
|
|
119
|
+
*
|
|
120
|
+
* @throws TypeError when `callExpr` is not a parsed `CallExpr` from this
|
|
121
|
+
* language adapter (a fabricated or synthetic view node has no syntax).
|
|
122
|
+
* @internal
|
|
123
|
+
*/
|
|
124
|
+
export declare function rawCallExprFor(callExpr: ShellNode): CallExpr;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridges command-level questions from ESLint view identity back to faithful
|
|
3
|
+
* syntax. `sh-ast/analyze` owns what a command invocation is and which command
|
|
4
|
+
* it runs; this module only builds the per-file index of the `CommandSite`s
|
|
5
|
+
* analysis reports, maps a lint `CallExpr` to its raw syntax node, and maps
|
|
6
|
+
* each site's raw `CallExpr` forward to its lint view node, so every rule gets
|
|
7
|
+
* the analysis answer rather than a local reinterpretation.
|
|
8
|
+
*/
|
|
9
|
+
import { enumerateCommands, resolveArgv0 } from 'sh-ast/analyze';
|
|
10
|
+
import { syntaxForNode, viewForSyntax, withoutCrlfCarriageReturn } from './syntax-view.js';
|
|
11
|
+
/**
|
|
12
|
+
* Builds the {@link CommandSiteIndex} for a whole parsed file with a single
|
|
13
|
+
* `enumerateCommands` call over the file's original syntax. Each site's
|
|
14
|
+
* `node` is the parser's own `CallExpr` object, the same object the lint view
|
|
15
|
+
* associates with its projected `CallExpr`, so lookups are by identity.
|
|
16
|
+
*
|
|
17
|
+
* Building a `CommandSite` by hand instead would duplicate analysis's choices
|
|
18
|
+
* about which words form argv and how they resolve, which is exactly the drift
|
|
19
|
+
* this bridge exists to prevent.
|
|
20
|
+
*
|
|
21
|
+
* The index is shared by every rule linting the file, and its data reaches
|
|
22
|
+
* rules: `getCommandSites` exposes each site's `context`, `argv0` and `argv`.
|
|
23
|
+
* Analysis also shares one `context` array and its frames between
|
|
24
|
+
* sibling commands. So every site is frozen in place here, before any rule
|
|
25
|
+
* can see it (see {@link freezeCommandSite}).
|
|
26
|
+
*
|
|
27
|
+
* @throws TypeError when `file` has no associated parsed syntax.
|
|
28
|
+
* @throws `ShAnalyzeMaxDepthError` (from `sh-ast/analyze`) when the file's
|
|
29
|
+
* nesting exceeds analysis's recursion guard. It propagates unchanged: a
|
|
30
|
+
* partial index would silently omit real commands, so callers fail closed.
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export function buildCommandSiteIndex(file) {
|
|
34
|
+
const syntax = syntaxForNode(file);
|
|
35
|
+
if (syntax?.Type !== 'File') {
|
|
36
|
+
throw new TypeError('expected a parsed shell File from this language adapter');
|
|
37
|
+
}
|
|
38
|
+
const sites = Object.freeze(enumerateCommands(syntax).map((site) => freezeCommandSite(withLfArgv(site))));
|
|
39
|
+
return { sites, byNode: new Map(sites.map((site) => [site.node, site])) };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `site` with each argv word resolved as its LF equivalent (see
|
|
43
|
+
* `withoutCrlfCarriageReturn`): on CRLF input, a static word that ends its
|
|
44
|
+
* line otherwise carries the terminator's `\r` (`ls\r`). Normalizing argv
|
|
45
|
+
* here, once, reaches every consumer, because `resolveArgv0` reads wrapper
|
|
46
|
+
* chains from `site.argv` and `getCommandName` reads `site.argv0`.
|
|
47
|
+
*/
|
|
48
|
+
function withLfArgv(site) {
|
|
49
|
+
const words = site.node.Args ?? [];
|
|
50
|
+
const argv = site.argv.map((resolution, index) => withoutCrlfCarriageReturn(words[index] ?? null, resolution));
|
|
51
|
+
if (argv.every((resolution, index) => resolution === site.argv[index]))
|
|
52
|
+
return site;
|
|
53
|
+
return { ...site, argv0: argv[0], argv };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Freezes the data a rule can reach through `site`: the site, its `context`
|
|
57
|
+
* array and each frame, its `argv` array and each resolution (`argv0` is
|
|
58
|
+
* `argv[0]`, frozen again harmlessly). `site.node` is the parser's syntax
|
|
59
|
+
* record, which rules never receive, and is left alone.
|
|
60
|
+
*
|
|
61
|
+
* Freezing in place, instead of copying, keeps every value identical to what
|
|
62
|
+
* analysis returned. That is safe because `enumerateCommands` builds these
|
|
63
|
+
* objects fresh on each call, and nothing in `sh-ast/analyze` writes to them
|
|
64
|
+
* after it returns: `resolveArgv0` only reads a site. A frame's fields are
|
|
65
|
+
* primitives or source text, so freezing each frame shallowly is complete.
|
|
66
|
+
*/
|
|
67
|
+
function freezeCommandSite(site) {
|
|
68
|
+
for (const frame of site.context)
|
|
69
|
+
Object.freeze(frame);
|
|
70
|
+
Object.freeze(site.context);
|
|
71
|
+
for (const word of site.argv)
|
|
72
|
+
Object.freeze(word);
|
|
73
|
+
Object.freeze(site.argv);
|
|
74
|
+
Object.freeze(site.argv0);
|
|
75
|
+
return Object.freeze(site);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Re-expresses every site in `index` in lint-view terms, in analysis's order.
|
|
79
|
+
* Each `node` comes from the syntax → view association recorded when `file`
|
|
80
|
+
* was projected, so no second walk of the view is needed and the node is the
|
|
81
|
+
* very object ESLint's traversal visits. The returned array and its entries
|
|
82
|
+
* are frozen, and the `context`, `argv0` and `argv` they carry were frozen
|
|
83
|
+
* when the index was built: the same array is shared by every rule linting
|
|
84
|
+
* this file.
|
|
85
|
+
*
|
|
86
|
+
* @throws Error when a site's `CallExpr` has no view node in `file`. Analysis
|
|
87
|
+
* of the same parse only reports parser nodes the projection saw, so this
|
|
88
|
+
* marks a broken invariant rather than a shell construct; failing loudly is
|
|
89
|
+
* safer than silently dropping a command.
|
|
90
|
+
* @internal
|
|
91
|
+
*/
|
|
92
|
+
export function viewCommandSites(file, index) {
|
|
93
|
+
return Object.freeze(index.sites.map((site) => {
|
|
94
|
+
const node = viewForSyntax(file, site.node);
|
|
95
|
+
if (node?.type !== 'CallExpr') {
|
|
96
|
+
throw new Error('command site has no CallExpr in the lint view of this file');
|
|
97
|
+
}
|
|
98
|
+
return Object.freeze({
|
|
99
|
+
node,
|
|
100
|
+
context: site.context,
|
|
101
|
+
argv0: site.argv0,
|
|
102
|
+
argv: site.argv,
|
|
103
|
+
resolveInvokedCommand: (options) => resolveArgv0(site, options),
|
|
104
|
+
});
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Returns the parser's own `CallExpr` behind a lint `CallExpr`.
|
|
109
|
+
*
|
|
110
|
+
* @throws TypeError when `callExpr` is not a parsed `CallExpr` from this
|
|
111
|
+
* language adapter (a fabricated or synthetic view node has no syntax).
|
|
112
|
+
* @internal
|
|
113
|
+
*/
|
|
114
|
+
export function rawCallExprFor(callExpr) {
|
|
115
|
+
const syntax = syntaxForNode(callExpr);
|
|
116
|
+
if (syntax?.Type !== 'CallExpr') {
|
|
117
|
+
throw new TypeError('expected a parsed shell CallExpr from this language adapter');
|
|
118
|
+
}
|
|
119
|
+
return syntax;
|
|
120
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects a source whose U+0080 made the parser silently drop text.
|
|
3
|
+
*
|
|
4
|
+
* mvdan/sh's reader uses U+0080 (`utf8.RuneSelf`) as its internal end-of-input marker. In some
|
|
5
|
+
* positions the parser reads it as an ordinary character and returns the whole tree; in others
|
|
6
|
+
* it stops there and returns a tree for only the text before it, with no error. A linter that
|
|
7
|
+
* accepted the second kind would skip every rule on the lost text, so those sources must be
|
|
8
|
+
* refused while the first kind keeps linting.
|
|
9
|
+
*
|
|
10
|
+
* The two are told apart by asking the parser what it does with the same source when U+0080 is
|
|
11
|
+
* an ordinary character: the parse is complete exactly when the printed tree is the same as the
|
|
12
|
+
* printed tree of the source with every U+0080 swapped for `X`. Text the parser dropped is
|
|
13
|
+
* missing from the first print and present in the second. Nothing is inferred from tree
|
|
14
|
+
* extents, so a trailing comment or heredoc body cannot make a truncated file look complete.
|
|
15
|
+
*/
|
|
16
|
+
import { type ParseOptions } from 'sh-ast';
|
|
17
|
+
/**
|
|
18
|
+
* The UTF-16 index of the first U+0080 whose presence makes the parser drop text, or
|
|
19
|
+
* `undefined` when `text` holds none or the parser handles every one of them completely.
|
|
20
|
+
*
|
|
21
|
+
* A source the parser rejects with an error is also `undefined`: that is a fatal parse error the
|
|
22
|
+
* caller reports through its normal path, not a silent loss. Every other doubt is a refusal:
|
|
23
|
+
* a source that parses but prints differently from its swapped counterpart is refused, and so is
|
|
24
|
+
* one whose swapped counterpart no longer parses, so an undecidable position is never accepted.
|
|
25
|
+
*
|
|
26
|
+
* @param text - The source about to be parsed.
|
|
27
|
+
* @param options - The parse options the caller will use, so the dialect matches.
|
|
28
|
+
*/
|
|
29
|
+
export declare function firstDroppedTextIndex(text: string, options: ParseOptions): number | undefined;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects a source whose U+0080 made the parser silently drop text.
|
|
3
|
+
*
|
|
4
|
+
* mvdan/sh's reader uses U+0080 (`utf8.RuneSelf`) as its internal end-of-input marker. In some
|
|
5
|
+
* positions the parser reads it as an ordinary character and returns the whole tree; in others
|
|
6
|
+
* it stops there and returns a tree for only the text before it, with no error. A linter that
|
|
7
|
+
* accepted the second kind would skip every rule on the lost text, so those sources must be
|
|
8
|
+
* refused while the first kind keeps linting.
|
|
9
|
+
*
|
|
10
|
+
* The two are told apart by asking the parser what it does with the same source when U+0080 is
|
|
11
|
+
* an ordinary character: the parse is complete exactly when the printed tree is the same as the
|
|
12
|
+
* printed tree of the source with every U+0080 swapped for `X`. Text the parser dropped is
|
|
13
|
+
* missing from the first print and present in the second. Nothing is inferred from tree
|
|
14
|
+
* extents, so a trailing comment or heredoc body cannot make a truncated file look complete.
|
|
15
|
+
*/
|
|
16
|
+
import { parseSync, printSync } from 'sh-ast';
|
|
17
|
+
const END_OF_INPUT_MARKER = '\u0080';
|
|
18
|
+
/** An ordinary one-UTF-16-unit word character, so offsets are the same in both parses. */
|
|
19
|
+
const ORDINARY_CHARACTER = 'X';
|
|
20
|
+
/** What the parser makes of a source: the printed tree, or `ERROR` when it does not parse. */
|
|
21
|
+
const ERROR = Symbol('parse error');
|
|
22
|
+
/**
|
|
23
|
+
* Prints `text` the way the parser sees it. Every U+0080 in the result is swapped for the
|
|
24
|
+
* ordinary character so the print of the original can be compared with the print of the
|
|
25
|
+
* swapped source.
|
|
26
|
+
*/
|
|
27
|
+
function printed(text, options) {
|
|
28
|
+
try {
|
|
29
|
+
const output = printSync(parseSync(text, options));
|
|
30
|
+
const decoded = typeof output === 'string' ? output : new TextDecoder().decode(output);
|
|
31
|
+
return decoded.replaceAll(END_OF_INPUT_MARKER, ORDINARY_CHARACTER);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return ERROR;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The UTF-16 index of the first U+0080 whose presence makes the parser drop text, or
|
|
39
|
+
* `undefined` when `text` holds none or the parser handles every one of them completely.
|
|
40
|
+
*
|
|
41
|
+
* A source the parser rejects with an error is also `undefined`: that is a fatal parse error the
|
|
42
|
+
* caller reports through its normal path, not a silent loss. Every other doubt is a refusal:
|
|
43
|
+
* a source that parses but prints differently from its swapped counterpart is refused, and so is
|
|
44
|
+
* one whose swapped counterpart no longer parses, so an undecidable position is never accepted.
|
|
45
|
+
*
|
|
46
|
+
* @param text - The source about to be parsed.
|
|
47
|
+
* @param options - The parse options the caller will use, so the dialect matches.
|
|
48
|
+
*/
|
|
49
|
+
export function firstDroppedTextIndex(text, options) {
|
|
50
|
+
const indices = [];
|
|
51
|
+
for (let at = text.indexOf(END_OF_INPUT_MARKER); at !== -1; at = text.indexOf(END_OF_INPUT_MARKER, at + 1)) {
|
|
52
|
+
indices.push(at);
|
|
53
|
+
}
|
|
54
|
+
if (indices.length === 0)
|
|
55
|
+
return undefined;
|
|
56
|
+
const ordinary = text.replaceAll(END_OF_INPUT_MARKER, ORDINARY_CHARACTER);
|
|
57
|
+
const expected = printed(ordinary, options);
|
|
58
|
+
const actual = printed(text, options);
|
|
59
|
+
if (actual === ERROR)
|
|
60
|
+
return undefined;
|
|
61
|
+
if (expected !== ERROR && actual === expected)
|
|
62
|
+
return undefined;
|
|
63
|
+
/** Whether keeping only the first `count` U+0080 characters (the rest swapped) still differs. */
|
|
64
|
+
const dropsText = (count) => {
|
|
65
|
+
let candidate = '';
|
|
66
|
+
let from = 0;
|
|
67
|
+
for (const index of indices.slice(0, count)) {
|
|
68
|
+
candidate += ordinary.slice(from, index) + END_OF_INPUT_MARKER;
|
|
69
|
+
from = index + 1;
|
|
70
|
+
}
|
|
71
|
+
candidate += ordinary.slice(from);
|
|
72
|
+
const result = printed(candidate, options);
|
|
73
|
+
return result === ERROR || expected === ERROR || result !== expected;
|
|
74
|
+
};
|
|
75
|
+
// The full source differs, so the smallest differing prefix of U+0080 characters exists; the
|
|
76
|
+
// search assumes that keeping more of them never repairs the parse, and only ever lands on a
|
|
77
|
+
// count that was itself observed to differ.
|
|
78
|
+
let low = 1;
|
|
79
|
+
let high = indices.length;
|
|
80
|
+
while (low < high) {
|
|
81
|
+
const middle = (low + high) >> 1;
|
|
82
|
+
if (dropsText(middle))
|
|
83
|
+
high = middle;
|
|
84
|
+
else
|
|
85
|
+
low = middle + 1;
|
|
86
|
+
}
|
|
87
|
+
return indices[high - 1];
|
|
88
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-break handling shared by rules, so a CRLF file lints like its LF
|
|
3
|
+
* equivalent: an edit that writes a line break uses the file's own style, and
|
|
4
|
+
* report text quoting source never prints the `\r` of a `\r\n`.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The line break an edit to `text` writes: the file's first line break, so a
|
|
8
|
+
* CRLF file stays CRLF, or `\n` when `text` has none.
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export declare function lineBreakOf(text: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* `excerpt` (source text a message quotes) with each `\r\n` written as `\n`,
|
|
14
|
+
* so a quoted multi-line construct reads the same in a CRLF file as in its LF
|
|
15
|
+
* equivalent.
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export declare function messageExcerpt(excerpt: string): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-break handling shared by rules, so a CRLF file lints like its LF
|
|
3
|
+
* equivalent: an edit that writes a line break uses the file's own style, and
|
|
4
|
+
* report text quoting source never prints the `\r` of a `\r\n`.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The line break an edit to `text` writes: the file's first line break, so a
|
|
8
|
+
* CRLF file stays CRLF, or `\n` when `text` has none.
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export function lineBreakOf(text) {
|
|
12
|
+
return /\r\n|\n|\r/u.exec(text)?.[0] ?? '\n';
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* `excerpt` (source text a message quotes) with each `\r\n` written as `\n`,
|
|
16
|
+
* so a quoted multi-line construct reads the same in a CRLF file as in its LF
|
|
17
|
+
* equivalent.
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export function messageExcerpt(excerpt) {
|
|
21
|
+
return excerpt.replace(/\r\n/gu, '\n');
|
|
22
|
+
}
|