@eslint-sh/eslint-plugin 0.5.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.
Files changed (256) hide show
  1. package/README.md +223 -17
  2. package/dist/command-harvest/builtins/bash.d.ts +36 -0
  3. package/dist/command-harvest/builtins/bash.js +98 -0
  4. package/dist/command-harvest/builtins/bats.d.ts +45 -0
  5. package/dist/command-harvest/builtins/bats.js +33 -0
  6. package/dist/command-harvest/builtins/index.d.ts +18 -0
  7. package/dist/command-harvest/builtins/index.js +39 -0
  8. package/dist/command-harvest/builtins/posix.d.ts +51 -0
  9. package/dist/command-harvest/builtins/posix.js +93 -0
  10. package/dist/command-harvest/harvest-commands.d.ts +66 -0
  11. package/dist/command-harvest/harvest-commands.js +97 -0
  12. package/dist/command-harvest/wrapper-table.d.ts +24 -0
  13. package/dist/command-harvest/wrapper-table.js +49 -0
  14. package/dist/configs/agent-authored.d.ts +41 -0
  15. package/dist/configs/agent-authored.js +33 -0
  16. package/dist/configs/bash-3-2.d.ts +28 -0
  17. package/dist/configs/bash-3-2.js +50 -0
  18. package/dist/configs/posix-portable.d.ts +40 -0
  19. package/dist/configs/posix-portable.js +52 -0
  20. package/dist/configs/recommended.d.ts +52 -0
  21. package/dist/configs/recommended.js +34 -0
  22. package/dist/declaration-conventions/comment-pragma.d.ts +33 -0
  23. package/dist/declaration-conventions/comment-pragma.js +153 -0
  24. package/dist/declaration-conventions/declaration-convention.d.ts +189 -0
  25. package/dist/declaration-conventions/declaration-convention.js +1 -0
  26. package/dist/declaration-conventions/read-declared-commands.d.ts +70 -0
  27. package/dist/declaration-conventions/read-declared-commands.js +128 -0
  28. package/dist/eslint-plugin-syntax-analyze.d.ts +23 -0
  29. package/dist/eslint-plugin-syntax.d.ts +27 -0
  30. package/dist/eslint-plugin.d.ts +1211 -52
  31. package/dist/index.d.ts +56 -8
  32. package/dist/index.js +53 -6
  33. package/dist/language/command-resolution.d.ts +124 -0
  34. package/dist/language/command-resolution.js +120 -0
  35. package/dist/language/end-of-input-marker.d.ts +29 -0
  36. package/dist/language/end-of-input-marker.js +88 -0
  37. package/dist/language/line-breaks.d.ts +18 -0
  38. package/dist/language/line-breaks.js +22 -0
  39. package/dist/language/lint-view-types.d.ts +104 -0
  40. package/dist/language/lint-view-types.js +1 -0
  41. package/dist/language/non-shell-file-guard.d.ts +35 -0
  42. package/dist/language/non-shell-file-guard.js +59 -0
  43. package/dist/language/selector-classes.d.ts +0 -1
  44. package/dist/language/selector-classes.js +0 -1
  45. package/dist/language/shell-language-options.d.ts +7 -1
  46. package/dist/language/shell-language-options.js +4 -4
  47. package/dist/language/shell-language.d.ts +10 -2
  48. package/dist/language/shell-language.js +59 -8
  49. package/dist/language/shell-source-code.d.ts +342 -5
  50. package/dist/language/shell-source-code.js +559 -30
  51. package/dist/language/source-coordinates.d.ts +13 -2
  52. package/dist/language/source-coordinates.js +68 -14
  53. package/dist/language/syntax-view.d.ts +60 -39
  54. package/dist/language/syntax-view.js +200 -45
  55. package/dist/language/word-resolution.d.ts +4 -3
  56. package/dist/language/word-resolution.js +4 -3
  57. package/dist/rules/compat/create-compat-rule.d.ts +59 -0
  58. package/dist/rules/compat/create-compat-rule.js +65 -0
  59. package/dist/rules/compat/declaration-options.d.ts +19 -0
  60. package/dist/rules/compat/declaration-options.js +110 -0
  61. package/dist/rules/compat/no-append-all-redirect.d.ts +13 -0
  62. package/dist/rules/compat/no-append-all-redirect.js +73 -0
  63. package/dist/rules/compat/no-associative-arrays.d.ts +12 -0
  64. package/dist/rules/compat/no-associative-arrays.js +39 -0
  65. package/dist/rules/compat/no-case-attributes.d.ts +12 -0
  66. package/dist/rules/compat/no-case-attributes.js +63 -0
  67. package/dist/rules/compat/no-case-fallthrough.d.ts +12 -0
  68. package/dist/rules/compat/no-case-fallthrough.js +67 -0
  69. package/dist/rules/compat/no-case-modification.d.ts +35 -0
  70. package/dist/rules/compat/no-case-modification.js +86 -0
  71. package/dist/rules/compat/no-coproc.d.ts +12 -0
  72. package/dist/rules/compat/no-coproc.js +47 -0
  73. package/dist/rules/compat/no-declare-g.d.ts +12 -0
  74. package/dist/rules/compat/no-declare-g.js +59 -0
  75. package/dist/rules/compat/no-double-bracket.d.ts +12 -0
  76. package/dist/rules/compat/no-double-bracket.js +52 -0
  77. package/dist/rules/compat/no-epoch-variables.d.ts +12 -0
  78. package/dist/rules/compat/no-epoch-variables.js +57 -0
  79. package/dist/rules/compat/no-fd-variable.d.ts +12 -0
  80. package/dist/rules/compat/no-fd-variable.js +48 -0
  81. package/dist/rules/compat/no-globstar.d.ts +12 -0
  82. package/dist/rules/compat/no-globstar.js +39 -0
  83. package/dist/rules/compat/no-lastpipe-semantics.d.ts +12 -0
  84. package/dist/rules/compat/no-lastpipe-semantics.js +53 -0
  85. package/dist/rules/compat/no-let.d.ts +12 -0
  86. package/dist/rules/compat/no-let.js +62 -0
  87. package/dist/rules/compat/no-mapfile.d.ts +13 -0
  88. package/dist/rules/compat/no-mapfile.js +122 -0
  89. package/dist/rules/compat/no-nameref.d.ts +12 -0
  90. package/dist/rules/compat/no-nameref.js +44 -0
  91. package/dist/rules/compat/no-negative-array-index.d.ts +13 -0
  92. package/dist/rules/compat/no-negative-array-index.js +123 -0
  93. package/dist/rules/compat/no-nounset-empty-array.d.ts +13 -0
  94. package/dist/rules/compat/no-nounset-empty-array.js +158 -0
  95. package/dist/rules/compat/no-parameter-transformation.d.ts +13 -0
  96. package/dist/rules/compat/no-parameter-transformation.js +123 -0
  97. package/dist/rules/compat/no-pipe-all.d.ts +13 -0
  98. package/dist/rules/compat/no-pipe-all.js +68 -0
  99. package/dist/rules/compat/no-printf-time-format.d.ts +12 -0
  100. package/dist/rules/compat/no-printf-time-format.js +99 -0
  101. package/dist/rules/compat/no-source.d.ts +13 -0
  102. package/dist/rules/compat/no-source.js +103 -0
  103. package/dist/rules/compat/no-test-v.d.ts +12 -0
  104. package/dist/rules/compat/no-test-v.js +280 -0
  105. package/dist/rules/compat/no-unicode-escape.d.ts +14 -0
  106. package/dist/rules/compat/no-unicode-escape.js +120 -0
  107. package/dist/rules/compat/no-wait-n.d.ts +12 -0
  108. package/dist/rules/compat/no-wait-n.js +73 -0
  109. package/dist/rules/compat/operator-range.d.ts +10 -0
  110. package/dist/rules/compat/operator-range.js +15 -0
  111. package/dist/rules/compat/posix-absent-command.d.ts +12 -0
  112. package/dist/rules/compat/posix-absent-command.js +134 -0
  113. package/dist/rules/compat/shopt-options.d.ts +9 -0
  114. package/dist/rules/compat/shopt-options.js +63 -0
  115. package/dist/rules/compat/version-gates.d.ts +76 -0
  116. package/dist/rules/compat/version-gates.js +197 -0
  117. package/dist/rules/create-rule.d.ts +91 -0
  118. package/dist/rules/create-rule.js +51 -0
  119. package/dist/rules/create-wrapper-rule.d.ts +166 -0
  120. package/dist/rules/create-wrapper-rule.js +407 -0
  121. package/dist/rules/define-shell-rule.d.ts +184 -0
  122. package/dist/rules/define-shell-rule.js +56 -0
  123. package/dist/rules/index.d.ts +26 -0
  124. package/dist/rules/index.js +88 -0
  125. package/dist/rules/max-script-complexity.d.ts +28 -0
  126. package/dist/rules/max-script-complexity.js +130 -0
  127. package/dist/rules/no-dynamic-invocation.d.ts +35 -0
  128. package/dist/rules/no-dynamic-invocation.js +117 -0
  129. package/dist/rules/no-eval.d.ts +13 -0
  130. package/dist/rules/no-eval.js +89 -0
  131. package/dist/rules/no-masked-exit-status.d.ts +21 -0
  132. package/dist/rules/no-masked-exit-status.js +667 -0
  133. package/dist/rules/no-secret-env-reads.d.ts +24 -0
  134. package/dist/rules/no-secret-env-reads.js +133 -0
  135. package/dist/rules/no-secret-flags.d.ts +32 -0
  136. package/dist/rules/no-secret-flags.js +162 -0
  137. package/dist/rules/no-unchecked-cd.d.ts +44 -0
  138. package/dist/rules/no-unchecked-cd.js +250 -0
  139. package/dist/rules/no-undeclared-commands.d.ts +20 -0
  140. package/dist/rules/no-undeclared-commands.js +115 -0
  141. package/dist/rules/no-unused-declarations.d.ts +23 -0
  142. package/dist/rules/no-unused-declarations.js +259 -0
  143. package/dist/rules/prefer-local.d.ts +16 -0
  144. package/dist/rules/prefer-local.js +249 -0
  145. package/dist/rules/quote-expansions.d.ts +13 -0
  146. package/dist/rules/quote-expansions.js +229 -0
  147. package/dist/rules/require-network-wrapper.d.ts +40 -0
  148. package/dist/rules/require-network-wrapper.js +244 -0
  149. package/dist/rules/require-shebang.d.ts +29 -0
  150. package/dist/rules/require-shebang.js +101 -0
  151. package/dist/rules/require-strict-mode.d.ts +52 -0
  152. package/dist/rules/require-strict-mode.js +177 -0
  153. package/dist/rules/require-tty-guard.d.ts +13 -0
  154. package/dist/rules/require-tty-guard.js +289 -0
  155. package/dist/rules/resolve-declaration-convention.d.ts +96 -0
  156. package/dist/rules/resolve-declaration-convention.js +109 -0
  157. package/dist/rules/resolve-network-wrapper.d.ts +19 -0
  158. package/dist/rules/resolve-network-wrapper.js +116 -0
  159. package/dist/rules/set-options.d.ts +109 -0
  160. package/dist/rules/set-options.js +201 -0
  161. package/dist/rules/shebang-interpreter.d.ts +18 -0
  162. package/dist/rules/shebang-interpreter.js +81 -0
  163. package/dist/rules/shebang-matches-dialect.d.ts +12 -0
  164. package/dist/rules/shebang-matches-dialect.js +69 -0
  165. package/dist/rules/transparent-wrappers.d.ts +52 -0
  166. package/dist/rules/transparent-wrappers.js +99 -0
  167. package/dist/rules/user-pattern.d.ts +63 -0
  168. package/dist/rules/user-pattern.js +184 -0
  169. package/dist/syntax/analyze.d.ts +19 -0
  170. package/dist/syntax/analyze.js +21 -0
  171. package/dist/syntax/index.d.ts +23 -0
  172. package/dist/syntax/index.js +25 -0
  173. package/dist/version.d.ts +0 -1
  174. package/dist/version.js +0 -1
  175. package/docs/compat.md +347 -0
  176. package/docs/custom-rule-tutorial.md +780 -34
  177. package/docs/declaration-conventions.md +478 -0
  178. package/docs/examples/audit-wrapper-pack.mjs +15 -0
  179. package/docs/examples/dep-line-convention.mjs +22 -0
  180. package/docs/examples/eslint.config.mjs +3 -2
  181. package/docs/examples/no-and-or-as-if-rule.mjs +61 -0
  182. package/docs/examples/no-backticks-rule-tester.mjs +106 -0
  183. package/docs/examples/no-backticks-rule.mjs +60 -0
  184. package/docs/examples/no-curl-in-functions-rule.mjs +53 -0
  185. package/docs/examples/no-function-keyword-rule.mjs +59 -0
  186. package/docs/examples/no-listed-commands-rule.ts +63 -0
  187. package/docs/examples/no-rm-rf-rule.mjs +26 -15
  188. package/docs/examples/no-unquoted-rm-variable-rule.ts +54 -0
  189. package/docs/examples/sidecar-convention.mjs +38 -0
  190. package/docs/examples/sidecar-snapshot-convention.mjs +26 -0
  191. package/docs/examples/sorted-declarations-rule.mjs +52 -0
  192. package/docs/reading-nodes.md +159 -0
  193. package/docs/rules/README.md +78 -0
  194. package/docs/rules/_template.md +83 -0
  195. package/docs/rules/compat/no-append-all-redirect.md +87 -0
  196. package/docs/rules/compat/no-associative-arrays.md +95 -0
  197. package/docs/rules/compat/no-case-attributes.md +82 -0
  198. package/docs/rules/compat/no-case-fallthrough.md +90 -0
  199. package/docs/rules/compat/no-case-modification.md +74 -0
  200. package/docs/rules/compat/no-coproc.md +76 -0
  201. package/docs/rules/compat/no-declare-g.md +80 -0
  202. package/docs/rules/compat/no-double-bracket.md +72 -0
  203. package/docs/rules/compat/no-epoch-variables.md +79 -0
  204. package/docs/rules/compat/no-fd-variable.md +78 -0
  205. package/docs/rules/compat/no-globstar.md +74 -0
  206. package/docs/rules/compat/no-lastpipe-semantics.md +73 -0
  207. package/docs/rules/compat/no-let.md +78 -0
  208. package/docs/rules/compat/no-mapfile.md +118 -0
  209. package/docs/rules/compat/no-nameref.md +81 -0
  210. package/docs/rules/compat/no-negative-array-index.md +87 -0
  211. package/docs/rules/compat/no-nounset-empty-array.md +103 -0
  212. package/docs/rules/compat/no-parameter-transformation.md +94 -0
  213. package/docs/rules/compat/no-pipe-all.md +85 -0
  214. package/docs/rules/compat/no-printf-time-format.md +75 -0
  215. package/docs/rules/compat/no-source.md +98 -0
  216. package/docs/rules/compat/no-test-v.md +82 -0
  217. package/docs/rules/compat/no-unicode-escape.md +91 -0
  218. package/docs/rules/compat/no-wait-n.md +85 -0
  219. package/docs/rules/max-script-complexity.md +109 -0
  220. package/docs/rules/no-dynamic-invocation.md +139 -0
  221. package/docs/rules/no-eval.md +106 -0
  222. package/docs/rules/no-masked-exit-status.md +239 -0
  223. package/docs/rules/no-secret-env-reads.md +173 -0
  224. package/docs/rules/no-secret-flags.md +169 -0
  225. package/docs/rules/no-unchecked-cd.md +146 -0
  226. package/docs/rules/no-undeclared-commands.md +141 -0
  227. package/docs/rules/no-unused-declarations.md +216 -0
  228. package/docs/rules/prefer-local.md +221 -0
  229. package/docs/rules/quote-expansions.md +157 -0
  230. package/docs/rules/require-network-wrapper.md +171 -0
  231. package/docs/rules/require-shebang.md +157 -0
  232. package/docs/rules/require-strict-mode.md +253 -0
  233. package/docs/rules/require-tty-guard.md +128 -0
  234. package/docs/rules/shebang-matches-dialect.md +120 -0
  235. package/docs/selectors.md +2 -2
  236. package/docs/shellcheck.md +86 -0
  237. package/docs/wrapper-rules.md +206 -0
  238. package/package.json +26 -20
  239. package/dist/index.d.ts.map +0 -1
  240. package/dist/index.js.map +0 -1
  241. package/dist/language/selector-classes.d.ts.map +0 -1
  242. package/dist/language/selector-classes.js.map +0 -1
  243. package/dist/language/shell-language-options.d.ts.map +0 -1
  244. package/dist/language/shell-language-options.js.map +0 -1
  245. package/dist/language/shell-language.d.ts.map +0 -1
  246. package/dist/language/shell-language.js.map +0 -1
  247. package/dist/language/shell-source-code.d.ts.map +0 -1
  248. package/dist/language/shell-source-code.js.map +0 -1
  249. package/dist/language/source-coordinates.d.ts.map +0 -1
  250. package/dist/language/source-coordinates.js.map +0 -1
  251. package/dist/language/syntax-view.d.ts.map +0 -1
  252. package/dist/language/syntax-view.js.map +0 -1
  253. package/dist/language/word-resolution.d.ts.map +0 -1
  254. package/dist/language/word-resolution.js.map +0 -1
  255. package/dist/version.d.ts.map +0 -1
  256. 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 { ShellFile, ShellNode, ShellComment, ShellPosition } from './language/syntax-view.js';
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: `languages.sh` provides
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
- * The empty `rules` registry lets consumers supply their own rules while
16
- * sharing parsing, selectors, source coordinates, and directive semantics.
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 _default: {
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 _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: `languages.sh` provides
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
- * The empty `rules` registry lets consumers supply their own rules while
14
- * sharing parsing, selectors, source coordinates, and directive semantics.
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
- export default {
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
- //# sourceMappingURL=index.js.map
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
+ }