From f57b9130bdcac0d964315514666cd7667befd90e Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Mon, 16 Jan 2023 22:45:27 +0100 Subject: [PATCH] Support imports and exports anywhere (#123) --- LICENSE | 2 +- src/exports.js | 46 ++++++++++++++++++++++++++++++-------------- src/imports.js | 21 +++++++++++++++----- src/shared.js | 4 ++-- test/exports.test.js | 36 ++++++++++++++++++++++++++++++++++ test/imports.test.js | 37 +++++++++++++++++++++++++++++++++++ 6 files changed, 124 insertions(+), 22 deletions(-) diff --git a/LICENSE b/LICENSE index 51fdd02..0389e43 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018, 2019, 2020, 2022 Simon Lydell +Copyright (c) 2018, 2019, 2020, 2022, 2023 Simon Lydell Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/exports.js b/src/exports.js index 76a6f29..925c650 100644 --- a/src/exports.js +++ b/src/exports.js @@ -14,21 +14,39 @@ module.exports = { sort: "Run autofix to sort these exports!", }, }, - create: (context) => ({ - Program: (programNode) => { - const sourceCode = context.getSourceCode(); - for (const chunk of shared.extractChunks(programNode, (node, lastNode) => - isPartOfChunk(node, lastNode, sourceCode) - )) { - maybeReportChunkSorting(chunk, context); - } - }, - ExportNamedDeclaration: (node) => { - if (node.source == null && node.declaration == null) { - maybeReportExportSpecifierSorting(node, context); + create: (context) => { + const parents = new Set(); + + const addParent = (node) => { + if (isExportFrom(node)) { + parents.add(node.parent); } - }, - }), + }; + + return { + ExportNamedDeclaration: (node) => { + if (node.source == null && node.declaration == null) { + maybeReportExportSpecifierSorting(node, context); + } else { + addParent(node); + } + }, + + ExportAllDeclaration: addParent, + + "Program:exit": () => { + const sourceCode = context.getSourceCode(); + for (const parent of parents) { + for (const chunk of shared.extractChunks(parent, (node, lastNode) => + isPartOfChunk(node, lastNode, sourceCode) + )) { + maybeReportChunkSorting(chunk, context); + } + } + parents.clear(); + }, + }; + }, }; function maybeReportChunkSorting(chunk, context) { diff --git a/src/imports.js b/src/imports.js index 5021950..d284a70 100644 --- a/src/imports.js +++ b/src/imports.js @@ -48,16 +48,27 @@ module.exports = { }, create: (context) => { const { groups: rawGroups = defaultGroups } = context.options[0] || {}; + const outerGroups = rawGroups.map((groups) => groups.map((item) => RegExp(item, "u")) ); + + const parents = new Set(); + return { - Program: (programNode) => { - for (const chunk of shared.extractChunks(programNode, (node) => - isImport(node) ? "PartOfChunk" : "NotPartOfChunk" - )) { - maybeReportChunkSorting(chunk, context, outerGroups); + ImportDeclaration: (node) => { + parents.add(node.parent); + }, + + "Program:exit": () => { + for (const parent of parents) { + for (const chunk of shared.extractChunks(parent, (node) => + isImport(node) ? "PartOfChunk" : "NotPartOfChunk" + )) { + maybeReportChunkSorting(chunk, context, outerGroups); + } } + parents.clear(); }, }; }, diff --git a/src/shared.js b/src/shared.js index dc4ac95..467ba54 100644 --- a/src/shared.js +++ b/src/shared.js @@ -2,12 +2,12 @@ // A “chunk” is a sequence of statements of a certain type with only comments // and whitespace between. -function extractChunks(programNode, isPartOfChunk) { +function extractChunks(parentNode, isPartOfChunk) { const chunks = []; let chunk = []; let lastNode = undefined; - for (const node of programNode.body) { + for (const node of parentNode.body) { const result = isPartOfChunk(node, lastNode); switch (result) { case "PartOfChunk": diff --git a/test/exports.test.js b/test/exports.test.js index 1e55a78..407b978 100644 --- a/test/exports.test.js +++ b/test/exports.test.js @@ -1106,6 +1106,42 @@ const typescriptTests = { }, errors: 1, }, + + // Exports inside module declarations. + { + code: input` + |export type {X} from "X"; + |export type {B} from "./B"; + | + |declare module 'my-module' { + | export type { PlatformPath, ParsedPath } from 'path'; + | export { type CopyOptions } from 'fs'; interface Something {} + | export {a, type type as type, z} from "../type"; + | // comment + | export * as d from "d" + |export {c} from "c"; /* + | */\texport {} from "b"; // b + |} + `, + output: (actual) => { + expect(actual).toMatchInlineSnapshot(` + |export type {B} from "./B"; + |export type {X} from "X"; + | + |declare module 'my-module' { + | export { type CopyOptions } from 'fs'; + | export type { ParsedPath,PlatformPath } from 'path';interface Something {} + | export {type type as type, a, z} from "../type"; + | // comment + |/* + | */→export {} from "b"; // b + |export {c} from "c"; + | export * as d from "d" + |} + `); + }, + errors: 4, + }, ], }; diff --git a/test/imports.test.js b/test/imports.test.js index 3815fb7..81eb01d 100644 --- a/test/imports.test.js +++ b/test/imports.test.js @@ -2000,6 +2000,43 @@ const typescriptTests = { }, errors: 1, }, + + // Imports inside module declarations. + { + code: input` + |import type { ParsedPath } from 'path'; + |import type { CopyOptions } from 'fs'; + | + |declare module 'my-module' { + | import type { PlatformPath, ParsedPath } from 'path'; + | import { type CopyOptions } from 'fs'; + | export function normalize(p: string): string; + | // comment + | import "d" + |import c from "c"; /* + | */\timport * as b from "b"; // b + |} + `, + output: (actual) => { + expect(actual).toMatchInlineSnapshot(` + |import type { CopyOptions } from 'fs'; + |import type { ParsedPath } from 'path'; + | + |declare module 'my-module' { + | import { type CopyOptions } from 'fs'; + | import type { ParsedPath,PlatformPath } from 'path'; + | export function normalize(p: string): string; + | // comment + | import "d" + | + |/* + | */→import * as b from "b"; // b + |import c from "c"; + |} + `); + }, + errors: 3, + }, ], };