From 9102e3d9bc9cd8b6bd6060a6ea943034f846da53 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Mon, 29 Apr 2024 12:18:48 +0900 Subject: [PATCH] feat: add `astro/no-exports-from-components` rule (#372) * feat: add `astro/no-exports-from-components` rule * Create chilled-carrots-glow.md --- .changeset/chilled-carrots-glow.md | 5 ++ README.md | 1 + docs/rules.md | 1 + docs/rules/no-exports-from-components.md | 41 +++++++++++ src/rules/no-exports-from-components.ts | 68 +++++++++++++++++++ src/utils/rules.ts | 2 + .../invalid/_config.json | 7 ++ .../invalid/export-all-01-errors.json | 7 ++ .../invalid/export-all-01-input.astro | 3 + .../invalid/export-default-01-errors.json | 7 ++ .../invalid/export-default-01-input.astro | 3 + .../invalid/export-default-02-errors.json | 7 ++ .../invalid/export-default-02-input.astro | 5 ++ .../invalid/export-default-03-errors.json | 7 ++ .../invalid/export-default-03-input.astro | 5 ++ .../invalid/named-export01-errors.json | 32 +++++++++ .../invalid/named-export01-input.astro | 11 +++ .../invalid/named-export02-errors.json | 32 +++++++++ .../invalid/named-export02-input.astro | 15 ++++ .../valid/_config.json | 7 ++ .../valid/export-all-01-input.astro | 3 + .../valid/export-default-01-input.astro | 5 ++ .../valid/named-export01-input.astro | 14 ++++ .../valid/named-export02-input.astro | 15 ++++ tests/src/rules/no-exports-from-components.ts | 16 +++++ tests/utils/utils.ts | 8 ++- tools/update-rulesets.ts | 2 +- 27 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 .changeset/chilled-carrots-glow.md create mode 100644 docs/rules/no-exports-from-components.md create mode 100644 src/rules/no-exports-from-components.ts create mode 100644 tests/fixtures/rules/no-exports-from-components/invalid/_config.json create mode 100644 tests/fixtures/rules/no-exports-from-components/invalid/export-all-01-errors.json create mode 100644 tests/fixtures/rules/no-exports-from-components/invalid/export-all-01-input.astro create mode 100644 tests/fixtures/rules/no-exports-from-components/invalid/export-default-01-errors.json create mode 100644 tests/fixtures/rules/no-exports-from-components/invalid/export-default-01-input.astro create mode 100644 tests/fixtures/rules/no-exports-from-components/invalid/export-default-02-errors.json create mode 100644 tests/fixtures/rules/no-exports-from-components/invalid/export-default-02-input.astro create mode 100644 tests/fixtures/rules/no-exports-from-components/invalid/export-default-03-errors.json create mode 100644 tests/fixtures/rules/no-exports-from-components/invalid/export-default-03-input.astro create mode 100644 tests/fixtures/rules/no-exports-from-components/invalid/named-export01-errors.json create mode 100644 tests/fixtures/rules/no-exports-from-components/invalid/named-export01-input.astro create mode 100644 tests/fixtures/rules/no-exports-from-components/invalid/named-export02-errors.json create mode 100644 tests/fixtures/rules/no-exports-from-components/invalid/named-export02-input.astro create mode 100644 tests/fixtures/rules/no-exports-from-components/valid/_config.json create mode 100644 tests/fixtures/rules/no-exports-from-components/valid/export-all-01-input.astro create mode 100644 tests/fixtures/rules/no-exports-from-components/valid/export-default-01-input.astro create mode 100644 tests/fixtures/rules/no-exports-from-components/valid/named-export01-input.astro create mode 100644 tests/fixtures/rules/no-exports-from-components/valid/named-export02-input.astro create mode 100644 tests/src/rules/no-exports-from-components.ts diff --git a/.changeset/chilled-carrots-glow.md b/.changeset/chilled-carrots-glow.md new file mode 100644 index 00000000..2fd7debd --- /dev/null +++ b/.changeset/chilled-carrots-glow.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-astro": minor +--- + +feat: add `astro/no-exports-from-components` rule diff --git a/README.md b/README.md index c62cc00a..b518cc35 100644 --- a/README.md +++ b/README.md @@ -400,6 +400,7 @@ These rules relate to possible syntax or logic errors in Astro component code: | [astro/no-deprecated-astro-fetchcontent](https://ota-meshi.github.io/eslint-plugin-astro/rules/no-deprecated-astro-fetchcontent/) | disallow using deprecated `Astro.fetchContent()` | ⭐🔧 | | [astro/no-deprecated-astro-resolve](https://ota-meshi.github.io/eslint-plugin-astro/rules/no-deprecated-astro-resolve/) | disallow using deprecated `Astro.resolve()` | ⭐ | | [astro/no-deprecated-getentrybyslug](https://ota-meshi.github.io/eslint-plugin-astro/rules/no-deprecated-getentrybyslug/) | disallow using deprecated `getEntryBySlug()` | ⭐ | +| [astro/no-exports-from-components](https://ota-meshi.github.io/eslint-plugin-astro/rules/no-exports-from-components/) | disallow value export | | | [astro/no-unused-define-vars-in-style](https://ota-meshi.github.io/eslint-plugin-astro/rules/no-unused-define-vars-in-style/) | disallow unused `define:vars={...}` in `style` tag | ⭐ | | [astro/valid-compile](https://ota-meshi.github.io/eslint-plugin-astro/rules/valid-compile/) | disallow warnings when compiling. | ⭐ | diff --git a/docs/rules.md b/docs/rules.md index 7e604bae..bf91d007 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -19,6 +19,7 @@ These rules relate to possible syntax or logic errors in Astro component code: | [astro/no-deprecated-astro-fetchcontent](./rules/no-deprecated-astro-fetchcontent.md) | disallow using deprecated `Astro.fetchContent()` | ⭐🔧 | | [astro/no-deprecated-astro-resolve](./rules/no-deprecated-astro-resolve.md) | disallow using deprecated `Astro.resolve()` | ⭐ | | [astro/no-deprecated-getentrybyslug](./rules/no-deprecated-getentrybyslug.md) | disallow using deprecated `getEntryBySlug()` | ⭐ | +| [astro/no-exports-from-components](./rules/no-exports-from-components.md) | disallow value export | | | [astro/no-unused-define-vars-in-style](./rules/no-unused-define-vars-in-style.md) | disallow unused `define:vars={...}` in `style` tag | ⭐ | | [astro/valid-compile](./rules/valid-compile.md) | disallow warnings when compiling. | ⭐ | diff --git a/docs/rules/no-exports-from-components.md b/docs/rules/no-exports-from-components.md new file mode 100644 index 00000000..f1f6ebb4 --- /dev/null +++ b/docs/rules/no-exports-from-components.md @@ -0,0 +1,41 @@ +--- +title: "astro/no-exports-from-components" +description: "disallow value export" +--- + +# astro/no-exports-from-components + +> disallow value export + +- ❗ **_This rule has not been released yet._** + +## 📖 Rule Details + +This rule reports value exports from Astro components. +The use of typed exports are still allowed. + + + + + +```astro +--- +/* eslint astro/no-exports-from-components: "error" */ +/* ✓ GOOD */ +export type A = number | boolean +/* ✗ BAD */ +export const x = 42 +--- +``` + + + +## 🔧 Options + +Nothing. + +## 🔍 Implementation + +- [Rule source](https://github.com/ota-meshi/eslint-plugin-astro/blob/main/src/rules/no-exports-from-components.ts) +- [Test source](https://github.com/ota-meshi/eslint-plugin-astro/blob/main/tests/src/rules/no-exports-from-components.ts) +- [Test fixture sources](https://github.com/ota-meshi/eslint-plugin-astro/tree/main/tests/fixtures/rules/no-exports-from-components) diff --git a/src/rules/no-exports-from-components.ts b/src/rules/no-exports-from-components.ts new file mode 100644 index 00000000..06a06ebe --- /dev/null +++ b/src/rules/no-exports-from-components.ts @@ -0,0 +1,68 @@ +import type { TSESTree } from "@typescript-eslint/types" +import { createRule } from "../utils" +import { getSourceCode } from "../utils/compat" + +export default createRule("no-exports-from-components", { + meta: { + docs: { + description: "disallow value export", + category: "Possible Errors", + // TODO: Switch to recommended: true, in next major version + recommended: false, + }, + schema: [], + messages: { + disallowExport: "Exporting values from components is not allowed.", + }, + type: "problem", + }, + create(context) { + const sourceCode = getSourceCode(context) + if (!sourceCode.parserServices.isAstro) { + return {} + } + + /** + * Verify for export declarations + */ + function verifyDeclaration( + node: + | TSESTree.ExportDefaultDeclaration["declaration"] + | TSESTree.ExportNamedDeclaration["declaration"], + ) { + if (!node) return + if (node.type.startsWith("TS") && !node.type.endsWith("Expression")) { + return + } + context.report({ + node, + messageId: "disallowExport", + }) + } + + return { + ExportAllDeclaration(node) { + if (node.exportKind === "type") return + context.report({ + node, + messageId: "disallowExport", + }) + }, + ExportDefaultDeclaration(node) { + if (node.exportKind === "type") return + verifyDeclaration(node.declaration) + }, + ExportNamedDeclaration(node) { + if (node.exportKind === "type") return + verifyDeclaration(node.declaration) + for (const spec of node.specifiers) { + if (spec.exportKind === "type") return + context.report({ + node: spec, + messageId: "disallowExport", + }) + } + }, + } + }, +}) diff --git a/src/utils/rules.ts b/src/utils/rules.ts index a0aa7e6d..1cda35a9 100644 --- a/src/utils/rules.ts +++ b/src/utils/rules.ts @@ -8,6 +8,7 @@ import noDeprecatedAstroCanonicalurl from "../rules/no-deprecated-astro-canonica import noDeprecatedAstroFetchcontent from "../rules/no-deprecated-astro-fetchcontent" import noDeprecatedAstroResolve from "../rules/no-deprecated-astro-resolve" import noDeprecatedGetentrybyslug from "../rules/no-deprecated-getentrybyslug" +import noExportsFromComponents from "../rules/no-exports-from-components" import noSetHtmlDirective from "../rules/no-set-html-directive" import noSetTextDirective from "../rules/no-set-text-directive" import noUnusedCssSelector from "../rules/no-unused-css-selector" @@ -26,6 +27,7 @@ export const rules = [ noDeprecatedAstroFetchcontent, noDeprecatedAstroResolve, noDeprecatedGetentrybyslug, + noExportsFromComponents, noSetHtmlDirective, noSetTextDirective, noUnusedCssSelector, diff --git a/tests/fixtures/rules/no-exports-from-components/invalid/_config.json b/tests/fixtures/rules/no-exports-from-components/invalid/_config.json new file mode 100644 index 00000000..0342354a --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/invalid/_config.json @@ -0,0 +1,7 @@ +{ + "languageOptions": { + "parserOptions": { + "parser": "@typescript-eslint/parser" + } + } +} diff --git a/tests/fixtures/rules/no-exports-from-components/invalid/export-all-01-errors.json b/tests/fixtures/rules/no-exports-from-components/invalid/export-all-01-errors.json new file mode 100644 index 00000000..c0648dd6 --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/invalid/export-all-01-errors.json @@ -0,0 +1,7 @@ +[ + { + "message": "Exporting values from components is not allowed.", + "line": 2, + "column": 1 + } +] diff --git a/tests/fixtures/rules/no-exports-from-components/invalid/export-all-01-input.astro b/tests/fixtures/rules/no-exports-from-components/invalid/export-all-01-input.astro new file mode 100644 index 00000000..8c297cd7 --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/invalid/export-all-01-input.astro @@ -0,0 +1,3 @@ +--- +export * from "./foo.js" +--- diff --git a/tests/fixtures/rules/no-exports-from-components/invalid/export-default-01-errors.json b/tests/fixtures/rules/no-exports-from-components/invalid/export-default-01-errors.json new file mode 100644 index 00000000..95322300 --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/invalid/export-default-01-errors.json @@ -0,0 +1,7 @@ +[ + { + "message": "Exporting values from components is not allowed.", + "line": 2, + "column": 16 + } +] diff --git a/tests/fixtures/rules/no-exports-from-components/invalid/export-default-01-input.astro b/tests/fixtures/rules/no-exports-from-components/invalid/export-default-01-input.astro new file mode 100644 index 00000000..557553b9 --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/invalid/export-default-01-input.astro @@ -0,0 +1,3 @@ +--- +export default {} +--- diff --git a/tests/fixtures/rules/no-exports-from-components/invalid/export-default-02-errors.json b/tests/fixtures/rules/no-exports-from-components/invalid/export-default-02-errors.json new file mode 100644 index 00000000..2f96e453 --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/invalid/export-default-02-errors.json @@ -0,0 +1,7 @@ +[ + { + "message": "Exporting values from components is not allowed.", + "line": 4, + "column": 16 + } +] diff --git a/tests/fixtures/rules/no-exports-from-components/invalid/export-default-02-input.astro b/tests/fixtures/rules/no-exports-from-components/invalid/export-default-02-input.astro new file mode 100644 index 00000000..170dca00 --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/invalid/export-default-02-input.astro @@ -0,0 +1,5 @@ +--- +export default function fn(): boolean +export default function fn(a: string): string +export default function fn(a?: string): string | boolean {} +--- diff --git a/tests/fixtures/rules/no-exports-from-components/invalid/export-default-03-errors.json b/tests/fixtures/rules/no-exports-from-components/invalid/export-default-03-errors.json new file mode 100644 index 00000000..95322300 --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/invalid/export-default-03-errors.json @@ -0,0 +1,7 @@ +[ + { + "message": "Exporting values from components is not allowed.", + "line": 2, + "column": 16 + } +] diff --git a/tests/fixtures/rules/no-exports-from-components/invalid/export-default-03-input.astro b/tests/fixtures/rules/no-exports-from-components/invalid/export-default-03-input.astro new file mode 100644 index 00000000..9c2d8d1f --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/invalid/export-default-03-input.astro @@ -0,0 +1,5 @@ +--- +export default class A { + private x(): void {} +} +--- diff --git a/tests/fixtures/rules/no-exports-from-components/invalid/named-export01-errors.json b/tests/fixtures/rules/no-exports-from-components/invalid/named-export01-errors.json new file mode 100644 index 00000000..79a1da78 --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/invalid/named-export01-errors.json @@ -0,0 +1,32 @@ +[ + { + "message": "Exporting values from components is not allowed.", + "line": 2, + "column": 10 + }, + { + "message": "Exporting values from components is not allowed.", + "line": 3, + "column": 10 + }, + { + "message": "Exporting values from components is not allowed.", + "line": 3, + "column": 13 + }, + { + "message": "Exporting values from components is not allowed.", + "line": 4, + "column": 8 + }, + { + "message": "Exporting values from components is not allowed.", + "line": 7, + "column": 8 + }, + { + "message": "Exporting values from components is not allowed.", + "line": 10, + "column": 8 + } +] diff --git a/tests/fixtures/rules/no-exports-from-components/invalid/named-export01-input.astro b/tests/fixtures/rules/no-exports-from-components/invalid/named-export01-input.astro new file mode 100644 index 00000000..f6e9e33a --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/invalid/named-export01-input.astro @@ -0,0 +1,11 @@ +--- +export { A } from "./foo.js" +export { B, B2 } from "./foo.js" +export const C = { + // ... +} +export class D { + private x(): void {} +} +export function E(): void {} +--- diff --git a/tests/fixtures/rules/no-exports-from-components/invalid/named-export02-errors.json b/tests/fixtures/rules/no-exports-from-components/invalid/named-export02-errors.json new file mode 100644 index 00000000..f936fb6c --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/invalid/named-export02-errors.json @@ -0,0 +1,32 @@ +[ + { + "message": "Exporting values from components is not allowed.", + "line": 13, + "column": 10 + }, + { + "message": "Exporting values from components is not allowed.", + "line": 13, + "column": 13 + }, + { + "message": "Exporting values from components is not allowed.", + "line": 13, + "column": 16 + }, + { + "message": "Exporting values from components is not allowed.", + "line": 14, + "column": 10 + }, + { + "message": "Exporting values from components is not allowed.", + "line": 14, + "column": 13 + }, + { + "message": "Exporting values from components is not allowed.", + "line": 14, + "column": 16 + } +] diff --git a/tests/fixtures/rules/no-exports-from-components/invalid/named-export02-input.astro b/tests/fixtures/rules/no-exports-from-components/invalid/named-export02-input.astro new file mode 100644 index 00000000..61e7f34b --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/invalid/named-export02-input.astro @@ -0,0 +1,15 @@ +--- +import { A } from "./foo.js" +import { B, B2 } from "./foo.js" +const C = { + // ... +} +class D { + private x(): void {} +} + +function E(): void {} + +export { A, B, B2 } +export { C, D, E } +--- diff --git a/tests/fixtures/rules/no-exports-from-components/valid/_config.json b/tests/fixtures/rules/no-exports-from-components/valid/_config.json new file mode 100644 index 00000000..0342354a --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/valid/_config.json @@ -0,0 +1,7 @@ +{ + "languageOptions": { + "parserOptions": { + "parser": "@typescript-eslint/parser" + } + } +} diff --git a/tests/fixtures/rules/no-exports-from-components/valid/export-all-01-input.astro b/tests/fixtures/rules/no-exports-from-components/valid/export-all-01-input.astro new file mode 100644 index 00000000..2a3eaf75 --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/valid/export-all-01-input.astro @@ -0,0 +1,3 @@ +--- +export type * from "./foo.js" +--- diff --git a/tests/fixtures/rules/no-exports-from-components/valid/export-default-01-input.astro b/tests/fixtures/rules/no-exports-from-components/valid/export-default-01-input.astro new file mode 100644 index 00000000..2221d243 --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/valid/export-default-01-input.astro @@ -0,0 +1,5 @@ +--- +export default interface A { + a: string +} +--- diff --git a/tests/fixtures/rules/no-exports-from-components/valid/named-export01-input.astro b/tests/fixtures/rules/no-exports-from-components/valid/named-export01-input.astro new file mode 100644 index 00000000..278ef71a --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/valid/named-export01-input.astro @@ -0,0 +1,14 @@ +--- +export { type A } from "./foo.js" +export type { B, B2 } from "./foo.js" +export type C = { + // ... +} +export interface D { + a(): void +} +export const enum E { + x = 1, + y = 2, +} +--- diff --git a/tests/fixtures/rules/no-exports-from-components/valid/named-export02-input.astro b/tests/fixtures/rules/no-exports-from-components/valid/named-export02-input.astro new file mode 100644 index 00000000..7bf81164 --- /dev/null +++ b/tests/fixtures/rules/no-exports-from-components/valid/named-export02-input.astro @@ -0,0 +1,15 @@ +--- +import type { A } from "./foo.js" +import type { B, B2 } from "./foo.js" +type C = { + // ... +} +interface D { + a(): void +} + +enum E {} + +export type { A, B, B2 } +export { type C, type D, type E } +--- diff --git a/tests/src/rules/no-exports-from-components.ts b/tests/src/rules/no-exports-from-components.ts new file mode 100644 index 00000000..dae5fa21 --- /dev/null +++ b/tests/src/rules/no-exports-from-components.ts @@ -0,0 +1,16 @@ +import { RuleTester } from "../../utils/eslint-compat" +import rule from "../../../src/rules/no-exports-from-components" +import { loadTestCases } from "../../utils/utils" + +const tester = new RuleTester({ + languageOptions: { + ecmaVersion: 2020, + sourceType: "module", + }, +}) + +tester.run( + "no-exports-from-components", + rule as any, + loadTestCases("no-exports-from-components"), +) diff --git a/tests/utils/utils.ts b/tests/utils/utils.ts index 277c0694..a0fd47e7 100644 --- a/tests/utils/utils.ts +++ b/tests/utils/utils.ts @@ -231,8 +231,14 @@ function getConfig(ruleName: string, inputFile: string) { } if (config && typeof config === "object") { return Object.assign( - { languageOptions: { parser: astroESLintParser } }, + {}, config, + { + languageOptions: { + parser: astroESLintParser, + ...config?.languageOptions, + }, + }, { code, filename }, ) } diff --git a/tools/update-rulesets.ts b/tools/update-rulesets.ts index 0504692e..986e000e 100644 --- a/tools/update-rulesets.ts +++ b/tools/update-rulesets.ts @@ -116,7 +116,7 @@ export default [ { plugins: { get astro(): ESLint.Plugin { - // eslint-disable-next-line @typescript-eslint/no-require-imports -- ignore + // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -- ignore return (plugin ??= require("../../plugin").plugin) }, },