@putout/printer 17.12.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,11 @@
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
+
1
9
  2026.03.03, v17.12.0
2
10
 
3
11
  feature:
@@ -38,4 +38,3 @@ export const AssignmentExpression = maybeParens({
38
38
 
39
39
  AssignmentExpression.printLeadingCommentLine = printLeadingCommentLine;
40
40
  AssignmentExpression.printLeadingCommentBlock = printLeadingCommentBlock;
41
-
@@ -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 = {
@@ -74,22 +89,3 @@ const isNextAssign = (path) => {
74
89
 
75
90
  return isAssignmentExpression(next.node.expression);
76
91
  };
77
-
78
- function isInsideBlockLike(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,11 +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();
19
- export const isInsideBlockLike = (path) => /^(Program|BlockStatement|TSModuleBlock|SwitchCase)$/.test(path.parentPath.type);
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
+ };
20
34
 
21
35
  export const isNext = (path) => {
22
36
  const next = path.getNextSibling();
@@ -27,6 +41,10 @@ export const isNext = (path) => {
27
41
  return !next.isEmptyStatement();
28
42
  };
29
43
 
44
+ export const hasBody = ({node}) => node.body.body.length;
45
+
46
+ export const hasEmptyBody = (path) => !path.node.body.length;
47
+
30
48
  export const isNextTry = (path) => {
31
49
  return isTryStatement(path.getNextSibling());
32
50
  };
@@ -178,4 +196,3 @@ export const hasLeadingComment = (path) => path.node?.leadingComments?.length;
178
196
 
179
197
  export const noTrailingComment = (path) => !path.node.trailingComments?.length;
180
198
  export const noLeadingComment = (path) => !path.node.leadingComments?.length;
181
-
@@ -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
 
@@ -6,21 +6,28 @@ import {
6
6
  isNewlineBetweenSiblings,
7
7
  exists,
8
8
  noTrailingComment,
9
- isInsideBlockLike,
9
+ isParentBlockLike,
10
10
  } from '#is';
11
11
  import {maybeSpaceAfterKeyword} from './maybe-space-after-keyword.js';
12
12
  import {isConcatenation} from '../../expressions/binary-expression/concatenate.js';
13
13
  import {parseLeadingComments} from '../../comment/comment.js';
14
14
  import {maybeDeclare} from '../../maybe/maybe-declare.js';
15
+ import {createTypeChecker} from '../../type-checker/type-checker.js';
15
16
 
16
17
  const {isExportDeclaration} = types;
17
18
 
18
19
  const isParentTSModuleBlock = (path) => path.parentPath.isTSModuleBlock();
19
- const isParentBlock = (path) => /Program|BlockStatement|Export|LabeledStatement/.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(isInsideBlockLike(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.12.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"