@putout/printer 17.11.0 → 17.13.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/ChangeLog CHANGED
@@ -1,3 +1,17 @@
1
+ 2026.03.04, v17.13.0
2
+
3
+ feature:
4
+ - 4af358d @putout/printer: type-checker: expression
5
+ - e3da216 @putout/printer: type-checker: improve dsl
6
+ - 93986ce @putout/printer: type checker: add
7
+ - 6a86e86 @putout/printer: type-checker: add
8
+
9
+ 2026.03.03, v17.12.0
10
+
11
+ feature:
12
+ - aa951ef @putout/printer: IfStatement: trailing comments
13
+ - 3c15683 @putout/printer: isParentBlock: reuse
14
+
1
15
  2026.03.03, v17.11.0
2
16
 
3
17
  feature:
@@ -1,4 +1,8 @@
1
1
  import {maybeParens} from '#maybe-parens';
2
+ import {
3
+ isParentBlock,
4
+ isParentProgram,
5
+ } from '#is';
2
6
  import {condition} from './maybe-parens-condition.js';
3
7
  import {printSeparator} from './print-separator.js';
4
8
  import {
@@ -8,8 +12,6 @@ import {
8
12
  printLeadingCommentBlock,
9
13
  } from './assignment-expression-comments.js';
10
14
 
11
- const isInsideBlock = ({parentPath}) => /BlockStatement|Program/.test(parentPath.type);
12
-
13
15
  export const AssignmentExpression = maybeParens({
14
16
  checkParens: false,
15
17
  condition,
@@ -25,7 +27,7 @@ export const AssignmentExpression = maybeParens({
25
27
  printSeparator(path, printer);
26
28
  print('__right');
27
29
 
28
- if (isInsideBlock(path)) {
30
+ if (isParentBlock(path) || isParentProgram(path)) {
29
31
  print(';');
30
32
  print.breakline();
31
33
  }
@@ -1,22 +1,18 @@
1
1
  import {types} from '@putout/babel';
2
- import {isNext} from '#is';
2
+ import {
3
+ hasBody,
4
+ isInsideTSModuleBlock,
5
+ isNext,
6
+ } from '#is';
3
7
  import {markAfter} from '#mark';
4
8
  import {maybeDeclare} from '../../maybe/maybe-declare.js';
5
9
  import {parseComments} from '../../comment/comment.js';
6
10
  import {maybeDecorators} from '../../maybe/maybe-decorators.js';
7
11
 
8
- const isInsideTSModuleBlock = (path) => {
9
- return isTSModuleBlock(path.parentPath.parentPath);
10
- };
11
-
12
- const {
13
- isFunction,
14
- isTSModuleBlock,
15
- } = types;
12
+ const {isFunction} = types;
16
13
 
17
14
  const isInsideExport = ({parentPath}) => parentPath.isExportDeclaration();
18
15
  const isFunctionLike = (path) => isFunction(path.parentPath.parentPath);
19
- const hasBody = ({node}) => node.body.body.length;
20
16
 
21
17
  const classVisitor = maybeDecorators((path, printer, semantics) => {
22
18
  const {
@@ -1,21 +1,36 @@
1
1
  import {types} from '@putout/babel';
2
- import {isNext, isNextParent} from '#is';
3
2
  import {printParams} from '#print-params';
4
3
  import {markAfter} from '#mark';
5
-
6
- const not = (fn) => (...a) => !fn(...a);
7
- const notInsideExportDefaultWithBody = not(isInsideExportDefaultWithBody);
4
+ import {
5
+ isInsideTSModuleBlock,
6
+ isNext,
7
+ isNextParent,
8
+ } from '#is';
9
+ import {createTypeChecker} from '../../type-checker/type-checker.js';
8
10
 
9
11
  const {
10
12
  isAssignmentExpression,
11
- isTSModuleBlock,
12
- isBlockStatement,
13
13
  isExportNamedDeclaration,
14
14
  isExpressionStatement,
15
15
  isFunctionDeclaration,
16
- isExportDefaultDeclaration,
17
16
  } = types;
18
17
 
18
+ const hasFnBody = ({node}) => node.body.body.length;
19
+
20
+ const isInsideExportDefaultWithBody = createTypeChecker([
21
+ '-: parentPath -> !ExportDefaultDeclaration',
22
+ ['+', hasFnBody],
23
+ ]);
24
+
25
+ const isInsideBlockLike = createTypeChecker([
26
+ ['+', isInsideTSModuleBlock],
27
+ '-: parentPath -> !BlockStatement',
28
+ ['+: -> !', hasFnBody],
29
+ ]);
30
+
31
+ const not = (fn) => (...a) => !fn(...a);
32
+ const notInsideExportDefaultWithBody = not(isInsideExportDefaultWithBody);
33
+
19
34
  const isInsideNamedExport = ({parentPath}) => isExportNamedDeclaration(parentPath);
20
35
 
21
36
  export const FunctionDeclaration = {
@@ -51,7 +66,7 @@ export const FunctionDeclaration = {
51
66
  print.space();
52
67
  print('__body');
53
68
  },
54
- afterSatisfy: () => [isNext, isNextParent, isInsideBlockStatement],
69
+ afterSatisfy: () => [isNext, isNextParent, isInsideBlockLike],
55
70
  after(path, {indent, maybe}) {
56
71
  if (isNextAssign(path) || isNextFunction(path) || isNext(path))
57
72
  indent();
@@ -74,22 +89,3 @@ const isNextAssign = (path) => {
74
89
 
75
90
  return isAssignmentExpression(next.node.expression);
76
91
  };
77
-
78
- function isInsideBlockStatement(path) {
79
- const {parentPath} = path;
80
-
81
- if (isTSModuleBlock(parentPath.parentPath))
82
- return true;
83
-
84
- if (!isBlockStatement(parentPath))
85
- return false;
86
-
87
- return !path.node.body.body.length;
88
- }
89
-
90
- function isInsideExportDefaultWithBody(path) {
91
- if (!isExportDefaultDeclaration(path.parentPath))
92
- return false;
93
-
94
- return path.node.body.body.length;
95
- }
@@ -1,4 +1,5 @@
1
1
  import {types} from '@putout/babel';
2
+ import {createTypeChecker} from './type-checker/type-checker.js';
2
3
 
3
4
  const {
4
5
  isStringLiteral,
@@ -12,10 +13,24 @@ const {
12
13
  isObjectExpression,
13
14
  isLabeledStatement,
14
15
  isTryStatement,
16
+ isProgram,
17
+ isBlockStatement,
18
+ isTSModuleBlock,
15
19
  } = types;
16
20
 
17
- export const isParentProgram = (path) => path.parentPath?.isProgram();
18
- export const isParentBlock = (path) => path.parentPath.isBlockStatement();
21
+ export const isParentProgram = (path) => isProgram(path.parentPath);
22
+ export const isParentBlock = (path) => isBlockStatement(path.parentPath);
23
+
24
+ export const isParentBlockLike = createTypeChecker('path.parentPath', [
25
+ 'Program',
26
+ 'BlockStatement',
27
+ 'TSModuleBlock',
28
+ 'SwitchCase',
29
+ ]);
30
+
31
+ export const isInsideTSModuleBlock = ({parentPath}) => {
32
+ return isTSModuleBlock(parentPath?.parentPath);
33
+ };
19
34
 
20
35
  export const isNext = (path) => {
21
36
  const next = path.getNextSibling();
@@ -26,6 +41,10 @@ export const isNext = (path) => {
26
41
  return !next.isEmptyStatement();
27
42
  };
28
43
 
44
+ export const hasBody = ({node}) => node.body.body.length;
45
+
46
+ export const hasEmptyBody = (path) => !path.node.body.length;
47
+
29
48
  export const isNextTry = (path) => {
30
49
  return isTryStatement(path.getNextSibling());
31
50
  };
@@ -6,6 +6,7 @@ import {
6
6
  isLast,
7
7
  exists,
8
8
  satisfy,
9
+ isParentBlock,
9
10
  } from '#is';
10
11
  import {parseComments} from '../../comment/comment.js';
11
12
  import {insideIfWithNoBody} from './inside-if-with-no-body.js';
@@ -18,7 +19,6 @@ const {
18
19
  isFunctionDeclaration,
19
20
  isExportDeclaration,
20
21
  isDoWhileStatement,
21
- isBlockStatement,
22
22
  isArrayExpression,
23
23
  } = types;
24
24
 
@@ -127,7 +127,6 @@ const NEWLINE = true;
127
127
  const NO_NEWLINE = false;
128
128
 
129
129
  const isInsideDoWhile = ({parentPath}) => isDoWhileStatement(parentPath);
130
- const isInsideBlock = ({parentPath}) => isBlockStatement(parentPath);
131
130
 
132
131
  const isNoNewline = satisfy([
133
132
  isInsideDoWhile,
@@ -136,7 +135,7 @@ const isNoNewline = satisfy([
136
135
  ]);
137
136
 
138
137
  function shouldAddNewlineAfter(path) {
139
- if (isInsideBlock(path))
138
+ if (isParentBlock(path))
140
139
  return NEWLINE;
141
140
 
142
141
  if (isNoNewline(path))
@@ -1,6 +1,8 @@
1
- import {isNext, isInsideIf} from '#is';
2
-
3
- const isInsideBlock = (path) => path.parentPath.isBlockStatement();
1
+ import {
2
+ isNext,
3
+ isInsideIf,
4
+ isParentBlock,
5
+ } from '#is';
4
6
 
5
7
  export const DebuggerStatement = {
6
8
  print(path, {print, indent}) {
@@ -9,7 +11,7 @@ export const DebuggerStatement = {
9
11
  },
10
12
  afterSatisfy: () => [
11
13
  isNext,
12
- isInsideBlock,
14
+ isParentBlock,
13
15
  isInsideIf,
14
16
  ],
15
17
  after(path, {print}) {
@@ -1,5 +1,5 @@
1
1
  import {types} from '@putout/babel';
2
- import {isNext} from '#is';
2
+ import {isNext, isParentBlock} from '#is';
3
3
 
4
4
  const {isBlockStatement} = types;
5
5
 
@@ -12,13 +12,18 @@ export const printTrailingCommentBlock = (path, printer, semantics, {printCommen
12
12
  };
13
13
 
14
14
  export const printTrailingCommentLine = (path, printer, semantics, {printComment}) => {
15
+ const hasNext = isNext(path);
15
16
  const {consequent} = path.node;
16
17
  const consequentIsBlock = isBlockStatement(consequent);
17
- const {print, maybe} = printer;
18
+ const {print} = printer;
18
19
 
19
20
  print.indent();
20
- maybe.print.space(!consequentIsBlock);
21
+
22
+ if (!hasNext && !consequentIsBlock)
23
+ print.space();
21
24
 
22
25
  printComment();
23
- maybe.print.newline(consequentIsBlock);
26
+
27
+ if (hasNext || isParentBlock(path))
28
+ print.newline();
24
29
  };
@@ -1,10 +1,11 @@
1
1
  import {types} from '@putout/babel';
2
+ import {markAfter} from '#mark';
2
3
  import {
3
4
  exists,
4
5
  isNext,
5
6
  isInsideIf,
7
+ hasEmptyBody,
6
8
  } from '#is';
7
- import {markAfter} from '#mark';
8
9
  import {
9
10
  printTrailingCommentBlock,
10
11
  printTrailingCommentLine,
@@ -42,8 +43,6 @@ const isInsideNestedBody = ({parentPath}) => {
42
43
  return parentPath.parentPath.type === 'BlockStatement';
43
44
  };
44
45
 
45
- const isEmptyBody = (path) => !path.node.body.length;
46
-
47
46
  const isLastEmptyInsideBody = (path) => {
48
47
  const {parentPath} = path;
49
48
 
@@ -85,7 +84,7 @@ export const IfStatement = {
85
84
  print(consequent);
86
85
 
87
86
  if (isInsideIf(path.parentPath) || isInsideNestedBody(path))
88
- maybe.print.newline(isEmptyBody(consequent));
87
+ maybe.print.newline(hasEmptyBody(consequent));
89
88
  } else {
90
89
  const is = !isEmptyConsequent(path);
91
90
 
@@ -1,5 +1,9 @@
1
1
  import {types} from '@putout/babel';
2
- import {isNext, isNextTry} from '#is';
2
+ import {
3
+ isNext,
4
+ isNextTry,
5
+ isParentBlock,
6
+ } from '#is';
3
7
 
4
8
  const {isExpressionStatement} = types;
5
9
 
@@ -34,6 +38,7 @@ const isNextExpression = (path) => {
34
38
  };
35
39
 
36
40
  export const CatchClause = (path, {print, maybe}) => {
41
+ const {parentPath} = path;
37
42
  const param = path.get('param');
38
43
  const body = path.get('body');
39
44
 
@@ -50,9 +55,5 @@ export const CatchClause = (path, {print, maybe}) => {
50
55
  }
51
56
 
52
57
  print(body);
53
- maybe.print.newline(isInsideBlock(path));
58
+ maybe.print.newline(isParentBlock(parentPath));
54
59
  };
55
-
56
- function isInsideBlock(path) {
57
- return path.parentPath.parentPath.isBlockStatement();
58
- }
@@ -6,21 +6,28 @@ import {
6
6
  isNewlineBetweenSiblings,
7
7
  exists,
8
8
  noTrailingComment,
9
+ isParentBlockLike,
9
10
  } from '#is';
10
11
  import {maybeSpaceAfterKeyword} from './maybe-space-after-keyword.js';
11
12
  import {isConcatenation} from '../../expressions/binary-expression/concatenate.js';
12
13
  import {parseLeadingComments} from '../../comment/comment.js';
13
14
  import {maybeDeclare} from '../../maybe/maybe-declare.js';
15
+ import {createTypeChecker} from '../../type-checker/type-checker.js';
14
16
 
15
17
  const {isExportDeclaration} = types;
16
18
 
17
19
  const isParentTSModuleBlock = (path) => path.parentPath.isTSModuleBlock();
18
- const isParentBlock = (path) => /Program|BlockStatement|Export|LabeledStatement/.test(path.parentPath.type);
19
- const isInsideBlock = (path) => /^(Program|BlockStatement|TSModuleBlock|SwitchCase)$/.test(path.parentPath.type);
20
20
  const isParentSwitchCase = (path) => path.parentPath.isSwitchCase();
21
21
  const isFirstInSwitch = (path) => path.parentPath.get('consequent.0') === path;
22
22
  const isParentIf = (path) => path.parentPath.isIfStatement();
23
23
 
24
+ const isParentBlock = createTypeChecker('path.parentPath', [
25
+ 'Program',
26
+ 'BlockStatement',
27
+ 'ExportNamedDeclaration',
28
+ 'LabeledStatement',
29
+ ]);
30
+
24
31
  export const VariableDeclaration = {
25
32
  beforeIf: shouldAddNewlineBefore,
26
33
  before(path, {print}) {
@@ -29,7 +36,7 @@ export const VariableDeclaration = {
29
36
  print: maybeDeclare((path, {maybe, store, write, traverse, print, indent}, semantics) => {
30
37
  const {maxVariablesInOneLine} = semantics;
31
38
 
32
- maybe.indent(isInsideBlock(path));
39
+ maybe.indent(isParentBlockLike(path));
33
40
 
34
41
  write(path.node.kind);
35
42
  maybeSpaceAfterKeyword(path, {
@@ -0,0 +1,130 @@
1
+ import {jessy} from 'jessy';
2
+
3
+ const {isArray} = Array;
4
+
5
+ const isString = (a) => typeof a === 'string';
6
+ const isFn = (a) => typeof a === 'function';
7
+
8
+ const equal = (not, a, b) => {
9
+ if (!isString(b))
10
+ return false;
11
+
12
+ if (not)
13
+ return a !== b;
14
+
15
+ return a === b;
16
+ };
17
+
18
+ const exec = (fn, not, a) => {
19
+ if (!isFn(fn))
20
+ return false;
21
+
22
+ const result = fn(a);
23
+
24
+ if (not)
25
+ return !result;
26
+
27
+ return result;
28
+ };
29
+
30
+ export const createTypeChecker = (deepness, typeNames) => {
31
+ if (!typeNames) {
32
+ typeNames = deepness;
33
+ deepness = '';
34
+ }
35
+
36
+ const tuples = parseTypeNames(typeNames);
37
+
38
+ return (path) => {
39
+ let i = deepness.split('.').length;
40
+
41
+ while (--i)
42
+ path = path?.parentPath;
43
+
44
+ if (!path)
45
+ return false;
46
+
47
+ for (const [operation, typeName] of tuples) {
48
+ const [result, selector, not] = parseOperation(operation);
49
+ let currentPath = path;
50
+
51
+ if (selector)
52
+ currentPath = jessy(selector, path);
53
+
54
+ const {type} = currentPath;
55
+
56
+ if (equal(not, type, typeName))
57
+ return result;
58
+
59
+ if (exec(typeName, not, currentPath))
60
+ return result;
61
+ }
62
+
63
+ return false;
64
+ };
65
+ };
66
+
67
+ const parseOperation = (operation) => {
68
+ const [result, command] = operation.split(':');
69
+ const parsedResult = parseResult(result);
70
+
71
+ if (!command)
72
+ return [parsedResult, '', ''];
73
+
74
+ const [selector, not] = command.split(' -> ');
75
+
76
+ return [parsedResult, selector.trim(), not];
77
+ };
78
+
79
+ const parseResult = (a) => a === '+';
80
+
81
+ function parseTypeNames(typeNames) {
82
+ const tuples = [];
83
+
84
+ if (isArray(typeNames)) {
85
+ for (const typeName of typeNames) {
86
+ if (isArray(typeName)) {
87
+ tuples.push(typeName);
88
+ continue;
89
+ }
90
+
91
+ if (isString(typeName) && typeName.includes(' -> ')) {
92
+ const tuple = createTuple(typeName);
93
+ tuples.push(tuple);
94
+ continue;
95
+ }
96
+
97
+ tuples.push(['+', typeName]);
98
+ }
99
+
100
+ return tuples;
101
+ }
102
+
103
+ for (const typeName of typeNames.include) {
104
+ tuples.push(['+', typeName]);
105
+ }
106
+
107
+ for (const typeName of typeNames.exclude) {
108
+ tuples.push(['-', typeName]);
109
+ }
110
+
111
+ return tuples;
112
+ }
113
+
114
+ function createTuple(typeName) {
115
+ const afterSplit = typeName.split(' ');
116
+ const typeWithNot = afterSplit.pop();
117
+ const array = typeWithNot.split('!');
118
+ const operation = afterSplit.join(' ');
119
+
120
+ if (array.length === 1)
121
+ return [
122
+ `${operation} `,
123
+ typeWithNot,
124
+ ];
125
+
126
+ return [
127
+ `${operation} !`,
128
+ array[1],
129
+ ];
130
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@putout/printer",
3
- "version": "17.11.0",
3
+ "version": "17.13.0",
4
4
  "type": "module",
5
5
  "author": "coderaiser <mnemonic.enemy@gmail.com> (https://github.com/coderaiser)",
6
6
  "description": "Simplest possible opinionated Babel AST printer for 🐊Putout",
@@ -38,6 +38,7 @@
38
38
  "@putout/operate": "^15.0.0",
39
39
  "@putout/operator-json": "^3.1.0",
40
40
  "fullstore": "^4.0.0",
41
+ "jessy": "^5.0.0",
41
42
  "just-snake-case": "^3.2.0",
42
43
  "parse-import-specifiers": "^1.0.1",
43
44
  "rendy": "^5.0.0"