Skip to content

Commit

Permalink
Support // prettier-ignore-start and // prettier-ignore--end comments
Browse files Browse the repository at this point in the history
Part of trivago/prettier-plugin-sort-imports#112

Note: Due to a limitation of how the AST is generated from the import nodes,
an empty lines is inserted before the range end comment:

// prettier-ignore-start
import "e";
import c from "c";
import { d } from "d";

// prettier-ignore-end
import { f } from "f";
import { g } from "g";
  • Loading branch information
blutorange committed Mar 17, 2022
1 parent cee20f7 commit 838a48c
Show file tree
Hide file tree
Showing 15 changed files with 240 additions and 28 deletions.
7 changes: 5 additions & 2 deletions src/preprocessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ImportDeclaration, isTSModuleDeclaration } from '@babel/types';
import { PrettierOptions } from './types';
import { getCodeFromAst } from './utils/get-code-from-ast';
import { getExperimentalParserPlugins } from './utils/get-experimental-parser-plugins';
import { getRangeIgnoredLines } from './utils/get-range-ignored-lines';
import { getSortedNodes } from './utils/get-sorted-nodes';

export function preprocessor(code: string, options: PrettierOptions) {
Expand All @@ -25,7 +26,7 @@ export function preprocessor(code: string, options: PrettierOptions) {

const ast = babelParser(code, parserOptions);
const interpreter = ast.program.interpreter;

traverse(ast, {
ImportDeclaration(path: NodePath<ImportDeclaration>) {
const tsModuleParent = path.findParent((p) =>
Expand All @@ -40,7 +41,9 @@ export function preprocessor(code: string, options: PrettierOptions) {
// short-circuit if there are no import declaration
if (importNodes.length === 0) return code;

const allImports = getSortedNodes(importNodes, {
const rangeIgnoredLines = getRangeIgnoredLines(ast.comments ?? []);

const allImports = getSortedNodes(importNodes, rangeIgnoredLines, {
importOrder,
importOrderCaseInsensitive,
importOrderSeparation,
Expand Down
24 changes: 23 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExpressionStatement, ImportDeclaration } from '@babel/types';
import { CommentBlock, CommentLine, ExpressionStatement, ImportDeclaration } from '@babel/types';
import { RequiredOptions } from 'prettier';

export interface PrettierOptions extends RequiredOptions {
Expand All @@ -21,6 +21,7 @@ export type ImportOrLine = ImportDeclaration | ExpressionStatement;

export type GetSortedNodes = (
nodes: ImportDeclaration[],
rangeIgnoredLines: Set<number>,
options: Pick<
PrettierOptions,
| 'importOrder'
Expand All @@ -30,3 +31,24 @@ export type GetSortedNodes = (
| 'importOrderSortSpecifiers'
>,
) => ImportOrLine[];

export type GetSortedNodesByImportOrder = (
nodes: ImportDeclaration[],
options: Pick<
PrettierOptions,
| 'importOrder'
| 'importOrderCaseInsensitive'
| 'importOrderSeparation'
| 'importOrderGroupNamespaceSpecifiers'
| 'importOrderSortSpecifiers'
>,
) => ImportOrLine[];

export type GetChunkTypeOfNode = (
node: ImportDeclaration,
rangeIgnoredLines: Set<number>
) => string;

export type GetRangeIgnoredLines = (
comments: readonly (CommentBlock | CommentLine)[]
) => Set<number>;
2 changes: 1 addition & 1 deletion src/utils/__tests__/get-all-comments-from-nodes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getSortedNodes } from '../get-sorted-nodes';
const getSortedImportNodes = (code: string, options?: ParserOptions) => {
const importNodes: ImportDeclaration[] = getImportNodes(code, options);

return getSortedNodes(importNodes, {
return getSortedNodes(importNodes, new Set(), {
importOrder: [],
importOrderCaseInsensitive: false,
importOrderSeparation: false,
Expand Down
22 changes: 15 additions & 7 deletions src/utils/__tests__/get-chunk-type-of-node.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,48 @@ import { getImportNodes } from "../get-import-nodes";
test('it classifies a default import as other', () => {
const importNodes = getImportNodes(`import a from "a";`);
expect(importNodes.length).toBe(1);
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeOther);
expect(getChunkTypeOfNode(importNodes[0], new Set())).toBe(chunkTypeOther);
});

test('it classifies a named import as other', () => {
const importNodes = getImportNodes(`import {a} from "a";`);
expect(importNodes.length).toBe(1);
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeOther);
expect(getChunkTypeOfNode(importNodes[0], new Set())).toBe(chunkTypeOther);
});

test('it classifies a side-effect import as unsortable', () => {
const importNodes = getImportNodes(`import "a";`);
expect(importNodes.length).toBe(1);
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeUnsortable);
expect(getChunkTypeOfNode(importNodes[0], new Set())).toBe(chunkTypeUnsortable);
});

test('it classifies a named import with a ignore next line comment as unsortable', () => {
const importNodes = getImportNodes(`// prettier-ignore
import {a} from "a";`);
expect(importNodes.length).toBe(1);
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeUnsortable);
expect(getChunkTypeOfNode(importNodes[0], new Set())).toBe(chunkTypeUnsortable);
});

test('it classifies a side-effect with a ignore next line comment as unsortable', () => {
const importNodes = getImportNodes(`// prettier-ignore
import "a";`);
expect(importNodes.length).toBe(1);
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeUnsortable);
expect(getChunkTypeOfNode(importNodes[0], new Set())).toBe(chunkTypeUnsortable);
});

test('it only applies the ignore next line comments to the next line', () => {
const importNodes = getImportNodes(`// prettier-ignore
import {b} from "b";
import {a} from "a";`);
expect(importNodes.length).toBe(2);
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeUnsortable);
expect(getChunkTypeOfNode(importNodes[1])).toBe(chunkTypeOther);
expect(getChunkTypeOfNode(importNodes[0], new Set())).toBe(chunkTypeUnsortable);
expect(getChunkTypeOfNode(importNodes[1], new Set())).toBe(chunkTypeOther);
});

test('it classifies a named import within a ranged ignore comment as unsortable', () => {
const importNodes = getImportNodes(`// prettier-ignore-start
import {a} from "a";
// prettier-ignore-end`);
expect(importNodes.length).toBe(1);
expect(getChunkTypeOfNode(importNodes[0], new Set([1, 2, 3]))).toBe(chunkTypeUnsortable);
});
2 changes: 1 addition & 1 deletion src/utils/__tests__/get-code-from-ast.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import k from 'k';
import a from 'a';
`;
const importNodes = getImportNodes(code);
const sortedNodes = getSortedNodes(importNodes, {
const sortedNodes = getSortedNodes(importNodes, new Set(), {
importOrder: [],
importOrderCaseInsensitive: false,
importOrderSeparation: false,
Expand Down
61 changes: 61 additions & 0 deletions src/utils/__tests__/get-range-ignored-lines.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { parse as babelParser } from '@babel/parser';
import { CommentBlock, CommentLine } from '@babel/types';
import { getRangeIgnoredLines } from "../get-range-ignored-lines";

function getComments(code: string): (CommentBlock | CommentLine)[] {
return babelParser(code, {
sourceType: 'module',
}).comments ?? [];
}

test('it does not find ranges when there are no ignore ranged', () => {
const comments = getComments(`import a from "a";`);
expect(comments.length).toBe(0);
expect(getRangeIgnoredLines(comments)).toEqual(new Set());
});

test('it finds a range delimited by start and end comments', () => {
const comments = getComments(`import a from "a";
// prettier-ignore-start
import b from "b";
import c from "c";
// prettier-ignore-end
import d from "d";`);
expect(comments.length).toBe(2);
expect(getRangeIgnoredLines(comments)).toEqual(new Set([2, 3, 4, 5]));
});

test('it includes the line on which a block comment is placed', () => {
const comments = getComments(`import a from "a";
import b from "b"; /* prettier-ignore-start */
/* prettier-ignore-end */ import c from "c";
import d from "d";`);
expect(comments.length).toBe(2);
expect(getRangeIgnoredLines(comments)).toEqual(new Set([2, 3]));
});

test('it considers only the first start and end comment', () => {
const comments = getComments(`import a from "a";
// prettier-ignore-start
import b from "b";
// prettier-ignore-start
import c from "c";
// prettier-ignore-end
import d from "d";
// prettier-ignore-end
import e from "e";`);
expect(comments.length).toBe(4);
expect(getRangeIgnoredLines(comments)).toEqual(new Set([2, 3, 4, 5, 6]));
});

test('it ignores unfinished start comments', () => {
const comments = getComments(`import a from "a";
// prettier-ignore-start
import b from "b";
// prettier-ignore-start
import c from "c";
import d from "d";
import e from "e";`);
expect(comments.length).toBe(2);
expect(getRangeIgnoredLines(comments)).toEqual(new Set([]));
});
16 changes: 8 additions & 8 deletions src/utils/__tests__/get-sorted-nodes-by-import-order.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import Xa from 'Xa';

test('it returns all sorted nodes', () => {
const result = getImportNodes(code);
const sorted = getSortedNodes(result, {
const sorted = getSortedNodes(result, new Set(), {
importOrder: [],
importOrderCaseInsensitive: false,
importOrderSeparation: false,
Expand Down Expand Up @@ -66,7 +66,7 @@ test('it returns all sorted nodes', () => {

test('it returns all sorted nodes case-insensitive', () => {
const result = getImportNodes(code);
const sorted = getSortedNodes(result, {
const sorted = getSortedNodes(result, new Set(), {
importOrder: [],
importOrderCaseInsensitive: true,
importOrderSeparation: false,
Expand Down Expand Up @@ -110,7 +110,7 @@ test('it returns all sorted nodes case-insensitive', () => {

test('it returns all sorted nodes with sort order', () => {
const result = getImportNodes(code);
const sorted = getSortedNodes(result, {
const sorted = getSortedNodes(result, new Set(), {
importOrder: ['^a$', '^t$', '^k$', '^B'],
importOrderCaseInsensitive: false,
importOrderSeparation: false,
Expand Down Expand Up @@ -154,7 +154,7 @@ test('it returns all sorted nodes with sort order', () => {

test('it returns all sorted nodes with sort order case-insensitive', () => {
const result = getImportNodes(code);
const sorted = getSortedNodes(result, {
const sorted = getSortedNodes(result, new Set(), {
importOrder: ['^a$', '^t$', '^k$', '^B'],
importOrderCaseInsensitive: true,
importOrderSeparation: false,
Expand Down Expand Up @@ -197,7 +197,7 @@ test('it returns all sorted nodes with sort order case-insensitive', () => {

test('it returns all sorted import nodes with sorted import specifiers', () => {
const result = getImportNodes(code);
const sorted = getSortedNodes(result, {
const sorted = getSortedNodes(result, new Set(), {
importOrder: ['^a$', '^t$', '^k$', '^B'],
importOrderCaseInsensitive: false,
importOrderSeparation: false,
Expand Down Expand Up @@ -240,7 +240,7 @@ test('it returns all sorted import nodes with sorted import specifiers', () => {

test('it returns all sorted import nodes with sorted import specifiers with case-insensitive ', () => {
const result = getImportNodes(code);
const sorted = getSortedNodes(result, {
const sorted = getSortedNodes(result, new Set(), {
importOrder: ['^a$', '^t$', '^k$', '^B'],
importOrderCaseInsensitive: true,
importOrderSeparation: false,
Expand Down Expand Up @@ -283,7 +283,7 @@ test('it returns all sorted import nodes with sorted import specifiers with case

test('it returns all sorted nodes with custom third party modules', () => {
const result = getImportNodes(code);
const sorted = getSortedNodes(result, {
const sorted = getSortedNodes(result, new Set(), {
importOrder: ['^a$', '<THIRD_PARTY_MODULES>', '^t$', '^k$'],
importOrderSeparation: false,
importOrderCaseInsensitive: true,
Expand All @@ -307,7 +307,7 @@ test('it returns all sorted nodes with custom third party modules', () => {

test('it returns all sorted nodes with namespace specifiers at the top', () => {
const result = getImportNodes(code);
const sorted = getSortedNodes(result, {
const sorted = getSortedNodes(result, new Set(), {
importOrder: [],
importOrderCaseInsensitive: false,
importOrderSeparation: false,
Expand Down
2 changes: 1 addition & 1 deletion src/utils/__tests__/get-sorted-nodes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import "se2";

test('it returns all sorted nodes, preserving the order side effect nodes', () => {
const result = getImportNodes(code);
const sorted = getSortedNodes(result, {
const sorted = getSortedNodes(result, new Set(), {
importOrder: [],
importOrderCaseInsensitive: false,
importOrderSeparation: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import a from 'a';

test('it should remove nodes from the original code', () => {
const importNodes = getImportNodes(code);
const sortedNodes = getSortedNodes(importNodes, {
const sortedNodes = getSortedNodes(importNodes,new Set(), {
importOrder: [],
importOrderCaseInsensitive: false,
importOrderSeparation: false,
Expand Down
10 changes: 8 additions & 2 deletions src/utils/get-chunk-type-of-node.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ImportDeclaration } from "@babel/types";
import { chunkTypeUnsortable, chunkTypeOther } from "../constants";
import { GetChunkTypeOfNode } from "../types";

/**
* Classifies an import declarations according to its properties, the
Expand All @@ -15,15 +16,20 @@ import { chunkTypeUnsortable, chunkTypeOther } from "../constants";
* - Otherwise, if the node is preceded by a comment exactly equal (up to
* leading and trailing spaces) the string `prettier-ignore`, classify the node
* as `unsortable`.
* - Otherwise, if the node is within a range of lines delimited by the start
* comment `prettier-ignore-start` and the end comment `prettier-ignore-end`,
* classify the node as `unsortable`.
* - Otherwise, classify the node as `sortable`.
* @param node An import declaration node to classify.
* @param rangeIgnoredLines Index of lines which are within an ignored range.
* @returns The type of the chunk into which the node should be put.
*/
export function getChunkTypeOfNode(node: ImportDeclaration): string {
export const getChunkTypeOfNode: GetChunkTypeOfNode = (node, rangeIgnoredLines) => {
const hasIgnoreNextNode = (node.leadingComments ?? [])
.some(comment => comment.value.trim() === "prettier-ignore");
const hasNoImportedSymbols = node.specifiers.length === 0;
return hasIgnoreNextNode || hasNoImportedSymbols
const isWithinIgnoredRange = rangeIgnoredLines.has(node.loc?.start.line ?? -1);
return hasIgnoreNextNode || isWithinIgnoredRange || hasNoImportedSymbols
? chunkTypeUnsortable
: chunkTypeOther
}
71 changes: 71 additions & 0 deletions src/utils/get-range-ignored-lines.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Comment } from "@babel/types";
import { GetRangeIgnoredLines } from "../types";

type Range = readonly [number, number];
type Ranges = readonly Range[];

export function isRangeStartComment(comment: Comment): boolean {
return comment.value.trim() === "prettier-ignore-start";
}

export function isRangeEndComment(comment: Comment): boolean {
return comment.value.trim() === "prettier-ignore-end";
}

function isRangeUnfinished(range: Range | undefined): boolean {
return range !== undefined && isNaN(range[1]);
}

function findIgnoredRanges(): [
(ranges: Ranges, comment: Comment) => Ranges,
Ranges
] {
return [
(ranges, comment) => {
const lastRangeUnfinished = isRangeUnfinished(ranges[ranges.length - 1]);
if (isRangeStartComment(comment)) {
return lastRangeUnfinished ? ranges : [...ranges, [comment.loc.start.line, NaN]]
}
if (isRangeEndComment(comment)) {
const head = ranges.slice(0, ranges.length - 1);
const tail = ranges[ranges.length - 1];
return lastRangeUnfinished ? [...head, [tail[0], comment.loc.end.line]] : ranges;
}
return ranges;
},
[]
];
}

function numbersBetween([start, end]: Range): readonly number[] {
const lines: number[] = [];
for (let line = start; line <= end; line += 1) {
lines.push(line);
}
return lines;
}

/**
* Given a list of comments, checks for ranged ignore comments and returns a
* set of all lines that should be ignored. The start of an ignored range is
* indicated by a `prettier-ignore-start` comment, then end of an ignored range
* by a `prettier-ignore-end` comment.
*
* Note that lines comments (`//`) should be used. This algorithm works with
* block comments (`/* ... * /`) too, but will consider the entire line ignored,
* even when the block comment is at the end of the line. This matches the
* current (2.4.1) prettier behavior for `prettier-ignore` (as prettier does not
* yet support ranged comments in JavaScript).
* @param comments List of comments to process.
* @return The set of all lines that fall inside a range delimited by range
* ignore comments. The lines with the comments themselves are included.
*/
export const getRangeIgnoredLines: GetRangeIgnoredLines = (comments) => {
const lines = comments
.reduce(...findIgnoredRanges())
.filter(range => !isRangeUnfinished(range))
.map(numbersBetween)
.flat();
return new Set(lines);
}

Loading

0 comments on commit 838a48c

Please sign in to comment.