prettier 0.22.0 → 1.1.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.
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@prettier/plugin-ruby",
3
- "version": "0.22.0",
3
+ "version": "1.1.0",
4
4
  "description": "prettier plugin for the Ruby programming language",
5
5
  "main": "src/ruby.js",
6
6
  "scripts": {
7
+ "check-format": "prettier --check '**/*'",
7
8
  "lint": "eslint --cache .",
8
- "print": "prettier --plugin=.",
9
9
  "test": "jest"
10
10
  },
11
11
  "repository": {
@@ -25,9 +25,9 @@
25
25
  "all-contributors-cli": "^6.14.1",
26
26
  "eslint": "^7.8.1",
27
27
  "eslint-config-prettier": "^7.0.0",
28
- "husky": "^5.0.4",
28
+ "husky": "^4.3.5",
29
29
  "jest": "^26.0.0",
30
- "pretty-quick": "^3.0.0"
30
+ "pretty-quick": "^3.1.0"
31
31
  },
32
32
  "eslintConfig": {
33
33
  "extends": [
@@ -50,6 +50,8 @@
50
50
  }
51
51
  },
52
52
  "jest": {
53
+ "globalSetup": "./test/js/globalSetup.js",
54
+ "globalTeardown": "./test/js/globalTeardown.js",
53
55
  "setupFilesAfterEnv": [
54
56
  "./test/js/setupTests.js"
55
57
  ],
@@ -61,6 +63,9 @@
61
63
  }
62
64
  },
63
65
  "prettier": {
64
- "trailingComma": "none"
66
+ "trailingComma": "none",
67
+ "plugins": [
68
+ "."
69
+ ]
65
70
  }
66
71
  }
@@ -31,7 +31,7 @@ const replaceNewlines = (doc) =>
31
31
  : currentDoc
32
32
  );
33
33
 
34
- const embed = (path, _print, textToDoc, _opts) => {
34
+ const embed = (path, print, textToDoc, _opts) => {
35
35
  const node = path.getValue();
36
36
 
37
37
  // Currently we only support embedded formatting on heredoc nodes
@@ -47,7 +47,7 @@ const embed = (path, _print, textToDoc, _opts) => {
47
47
 
48
48
  // Next, find the parser associated with this heredoc (if there is one). For
49
49
  // example, if you use <<~CSS, we'd hook it up to the css parser.
50
- const parser = parsers[beging.slice(3).toLowerCase()];
50
+ const parser = parsers[beging.body.slice(3).toLowerCase()];
51
51
  if (!parser) {
52
52
  return null;
53
53
  }
@@ -62,12 +62,16 @@ const embed = (path, _print, textToDoc, _opts) => {
62
62
 
63
63
  // If we're using a squiggly heredoc, then we can properly handle indentation
64
64
  // ourselves.
65
- if (beging[2] === "~") {
65
+ if (beging.body[2] === "~") {
66
66
  return concat([
67
- beging,
67
+ path.call(print, "beging"),
68
68
  lineSuffix(
69
69
  group(
70
- concat([indent(markAsRoot(formatted)), literalLineNoBreak, ending])
70
+ concat([
71
+ indent(markAsRoot(formatted)),
72
+ literalLineNoBreak,
73
+ ending.trim()
74
+ ])
71
75
  )
72
76
  )
73
77
  ]);
@@ -77,8 +81,8 @@ const embed = (path, _print, textToDoc, _opts) => {
77
81
  // content as it is.
78
82
  return markAsRoot(
79
83
  concat([
80
- beging,
81
- lineSuffix(group(concat([formatted, literalLineNoBreak, ending])))
84
+ path.call(print, "beging"),
85
+ lineSuffix(group(concat([formatted, literalLineNoBreak, ending.trim()])))
82
86
  ])
83
87
  );
84
88
  };
@@ -9,81 +9,56 @@ const {
9
9
  } = require("../prettier");
10
10
 
11
11
  const toProc = require("../toProc");
12
- const { docLength } = require("../utils");
13
-
14
- module.exports = {
15
- arg_paren: (path, opts, print) => {
16
- const argsNode = path.getValue().body[0];
17
- const { addTrailingCommas } = opts;
18
-
19
- if (argsNode === null) {
20
- return "";
21
- }
22
-
23
- // Here we can skip the entire rest of the method by just checking if it's
24
- // an args_forward node, as we're guaranteed that there are no other arg
25
- // nodes.
26
- if (argsNode.type === "args_forward") {
27
- return group(
12
+ const { getTrailingComma } = require("../utils");
13
+
14
+ function printArgParen(path, opts, print) {
15
+ const argsNode = path.getValue().body[0];
16
+
17
+ if (argsNode === null) {
18
+ return "";
19
+ }
20
+
21
+ // Here we can skip the entire rest of the method by just checking if it's
22
+ // an args_forward node, as we're guaranteed that there are no other arg
23
+ // nodes.
24
+ if (argsNode.type === "args_forward") {
25
+ return group(
26
+ concat([
27
+ "(",
28
+ indent(concat([softline, path.call(print, "body", 0)])),
29
+ softline,
30
+ ")"
31
+ ])
32
+ );
33
+ }
34
+
35
+ const args = path.call(print, "body", 0);
36
+ const hasBlock = argsNode.type === "args_add_block" && argsNode.body[1];
37
+
38
+ // Now here we return a doc that represents the whole grouped expression,
39
+ // including the surrouding parentheses.
40
+ return group(
41
+ concat([
42
+ "(",
43
+ indent(
28
44
  concat([
29
- "(",
30
- indent(concat([softline, path.call(print, "body", 0)])),
31
45
  softline,
32
- ")"
46
+ join(concat([",", line]), args),
47
+ getTrailingComma(opts) && !hasBlock ? ifBreak(",", "") : ""
33
48
  ])
34
- );
35
- }
36
-
37
- const args = path.call(print, "body", 0);
38
- const hasBlock = argsNode.type === "args_add_block" && argsNode.body[1];
39
-
40
- // These are the docs representing the actual arguments, without any
41
- // parentheses or surrounding lines yet added.
42
- let argsDocs = [
43
- join(concat([",", line]), args),
44
- addTrailingCommas && !hasBlock ? ifBreak(",", "") : ""
45
- ];
46
-
47
- // Here we're going to make a determination on whether or not we should put
48
- // a newline before the first argument. In some cases this makes the
49
- // appearance a little better. For example, instead of taking this input:
50
- //
51
- // foo(arg1, arg2).bar(arg1, arg2).baz(arg1)
52
- //
53
- // and transforming it into this:
54
- //
55
- // foo(arg1, arg2).bar(arg1, arg2).baz(
56
- // arg1
57
- // )
58
- //
59
- // it instead gets transformed into this:
60
- //
61
- // foo(arg1, arg2).bar(arg1, arg2)
62
- // .baz(arg1)
63
- //
64
- const maxDocLength = 15;
65
- const firstArgDoc = args[0];
66
-
67
- // prettier-ignore
68
- const shouldWrapLine =
69
- (args.reduce((sum, arg) => sum + docLength(arg), 0) > maxDocLength) ||
70
- (args.length == 1 && firstArgDoc.type === "group" && docLength(firstArgDoc) > maxDocLength) ||
71
- (firstArgDoc.type === "concat" && firstArgDoc.parts.some((part) => part.type === "group"));
72
-
73
- // Here we're going to get all of the docs representing the doc that's
74
- // inside the parentheses.
75
- if (shouldWrapLine) {
76
- argsDocs = [indent(concat([softline].concat(argsDocs))), softline];
77
- } else {
78
- argsDocs = [indent(concat(argsDocs))];
79
- }
80
-
81
- // Now here we return a doc that represents the whole grouped expression,
82
- // including the surrouding parentheses.
83
- return group(concat(["("].concat(argsDocs).concat(")")));
84
- },
85
- args: (path, opts, print) => {
86
- const args = path.map(print, "body");
49
+ ),
50
+ softline,
51
+ ")"
52
+ ])
53
+ );
54
+ }
55
+
56
+ function printArgs(path, { rubyToProc }, print) {
57
+ const args = path.map(print, "body");
58
+
59
+ // Don't bother trying to do any kind of fancy toProc transform if the
60
+ // option is disabled.
61
+ if (rubyToProc) {
87
62
  let blockNode = null;
88
63
 
89
64
  // Look up the chain to see if these arguments are contained within a
@@ -100,21 +75,26 @@ module.exports = {
100
75
  return blockNode;
101
76
  });
102
77
 
103
- const proc = blockNode && toProc(path, opts, blockNode);
78
+ const proc = blockNode && toProc(path, blockNode);
104
79
 
105
- // If we have a successful to_proc transformation, but we're part of an aref
106
- // node, that means it's something to the effect of
80
+ // If we have a successful to_proc transformation, but we're part of an
81
+ // aref node, that means it's something to the effect of
107
82
  //
108
83
  // foo[:bar].each(&:to_s)
109
84
  //
110
- // In this case we need to just return regular arguments, otherwise we would
111
- // end up putting &:to_s inside the brackets accidentally.
85
+ // In this case we need to just return regular arguments, otherwise we
86
+ // would end up putting &:to_s inside the brackets accidentally.
112
87
  if (proc && path.getParentNode(1).type !== "aref") {
113
88
  args.push(proc);
114
89
  }
90
+ }
115
91
 
116
- return args;
117
- },
92
+ return args;
93
+ }
94
+
95
+ module.exports = {
96
+ arg_paren: printArgParen,
97
+ args: printArgs,
118
98
  args_add_block: (path, opts, print) => {
119
99
  const parts = path.call(print, "body", 0);
120
100
 
@@ -1,7 +1,6 @@
1
1
  const {
2
2
  concat,
3
3
  group,
4
- hardline,
5
4
  ifBreak,
6
5
  indent,
7
6
  join,
@@ -9,6 +8,8 @@ const {
9
8
  softline
10
9
  } = require("../prettier");
11
10
 
11
+ const { getTrailingComma, printEmptyCollection } = require("../utils");
12
+
12
13
  // Checks that every argument within this args node is a string_literal node
13
14
  // that has no spaces or interpolations. This means we're dealing with an array
14
15
  // that looks something like:
@@ -16,31 +17,35 @@ const {
16
17
  // ['a', 'b', 'c']
17
18
  //
18
19
  function isStringArray(args) {
19
- return args.body.every((arg) => {
20
- // We want to verify that every node inside of this array is a string
21
- // literal. We also want to make sure none of them have comments attached.
22
- if (arg.type !== "string_literal" || arg.comments) {
23
- return false;
24
- }
25
-
26
- // If the string has multiple parts (meaning plain string content but also
27
- // interpolated content) then we know it's not a simple string.
28
- if (arg.body.length !== 1) {
29
- return false;
30
- }
31
-
32
- const part = arg.body[0];
33
-
34
- // If the only part of this string is not @tstring_content then it's
35
- // interpolated, so again we can return false.
36
- if (part.type !== "@tstring_content") {
37
- return false;
38
- }
39
-
40
- // Finally, verify that the string doesn't contain a space or an escape
41
- // character so that we know it can be put into a string literal array.
42
- return !part.body.includes(" ") && !part.body.includes("\\");
43
- });
20
+ return (
21
+ args.body.length > 1 &&
22
+ args.body.every((arg) => {
23
+ // We want to verify that every node inside of this array is a string
24
+ // literal. We also want to make sure none of them have comments attached.
25
+ if (arg.type !== "string_literal" || arg.comments) {
26
+ return false;
27
+ }
28
+
29
+ // If the string has multiple parts (meaning plain string content but also
30
+ // interpolated content) then we know it's not a simple string.
31
+ if (arg.body.length !== 1) {
32
+ return false;
33
+ }
34
+
35
+ const part = arg.body[0];
36
+
37
+ // If the only part of this string is not @tstring_content then it's
38
+ // interpolated, so again we can return false.
39
+ if (part.type !== "@tstring_content") {
40
+ return false;
41
+ }
42
+
43
+ // Finally, verify that the string doesn't contain a space, an escape
44
+ // character, or brackets so that we know it can be put into a string
45
+ // literal array.
46
+ return !/[\s\\[\]]/.test(part.body);
47
+ })
48
+ );
44
49
  }
45
50
 
46
51
  // Checks that every argument within this args node is a symbol_literal node (as
@@ -50,8 +55,9 @@ function isStringArray(args) {
50
55
  // [:a, :b, :c]
51
56
  //
52
57
  function isSymbolArray(args) {
53
- return args.body.every(
54
- (arg) => arg.type === "symbol_literal" && !arg.comments
58
+ return (
59
+ args.body.length > 1 &&
60
+ args.body.every((arg) => arg.type === "symbol_literal" && !arg.comments)
55
61
  );
56
62
  }
57
63
 
@@ -86,24 +92,6 @@ function printSpecialArray(start) {
86
92
  };
87
93
  }
88
94
 
89
- function printEmptyArrayWithComments(path, opts) {
90
- const arrayNode = path.getValue();
91
-
92
- const printComment = (commentPath, index) => {
93
- arrayNode.comments[index].printed = true;
94
- return opts.printer.printComment(commentPath);
95
- };
96
-
97
- return concat([
98
- "[",
99
- indent(
100
- concat([hardline, join(hardline, path.map(printComment, "comments"))])
101
- ),
102
- line,
103
- "]"
104
- ]);
105
- }
106
-
107
95
  // An array node is any literal array in Ruby. This includes all of the special
108
96
  // array literals as well as regular arrays. If it is a special array literal
109
97
  // then it will have one child that represents the special array, otherwise it
@@ -115,12 +103,12 @@ function printArray(path, opts, print) {
115
103
  // If there is no inner arguments node, then we're dealing with an empty
116
104
  // array, so we can go ahead and return.
117
105
  if (args === null) {
118
- return array.comments ? printEmptyArrayWithComments(path, opts) : "[]";
106
+ return printEmptyCollection(path, opts, "[", "]");
119
107
  }
120
108
 
121
109
  // If we have an array that contains only simple string literals with no
122
110
  // spaces or interpolation, then we're going to print a %w array.
123
- if (isStringArray(args)) {
111
+ if (opts.rubyArrayLiteral && isStringArray(args)) {
124
112
  const printString = (stringPath) => stringPath.call(print, "body", 0);
125
113
  const parts = path.map(printString, "body", 0, "body");
126
114
 
@@ -129,7 +117,7 @@ function printArray(path, opts, print) {
129
117
 
130
118
  // If we have an array that contains only simple symbol literals with no
131
119
  // interpolation, then we're going to print a %i array.
132
- if (isSymbolArray(args)) {
120
+ if (opts.rubyArrayLiteral && isSymbolArray(args)) {
133
121
  const printSymbol = (symbolPath) => symbolPath.call(print, "body", 0);
134
122
  const parts = path.map(printSymbol, "body", 0, "body");
135
123
 
@@ -153,7 +141,7 @@ function printArray(path, opts, print) {
153
141
  concat([
154
142
  softline,
155
143
  join(concat([",", line]), path.call(print, "body", 0)),
156
- opts.addTrailingCommas ? ifBreak(",", "") : ""
144
+ getTrailingComma(opts) ? ifBreak(",", "") : ""
157
145
  ])
158
146
  ),
159
147
  softline,
@@ -1,5 +1,5 @@
1
1
  const { concat, group, indent, join, line } = require("../prettier");
2
- const { concatBody, first, skipAssignIndent } = require("../utils");
2
+ const { first, skipAssignIndent } = require("../utils");
3
3
 
4
4
  function printAssign(path, opts, print) {
5
5
  const [_targetNode, valueNode] = path.getValue().body;
@@ -34,6 +34,6 @@ function printOpAssign(path, opts, print) {
34
34
  module.exports = {
35
35
  assign: printAssign,
36
36
  opassign: printOpAssign,
37
- var_field: concatBody,
37
+ var_field: first,
38
38
  var_ref: first
39
39
  };
@@ -6,40 +6,31 @@ const {
6
6
  indent,
7
7
  softline
8
8
  } = require("../prettier");
9
- const { concatBody, first, makeCall } = require("../utils");
9
+ const { first, makeCall, noIndent } = require("../utils");
10
10
 
11
11
  const toProc = require("../toProc");
12
12
 
13
- const chained = ["call", "method_add_arg"];
14
- const noIndent = ["array", "hash", "if", "method_add_block", "xstring_literal"];
13
+ const chained = ["call", "method_add_arg", "method_add_block"];
15
14
 
16
15
  function printCall(path, opts, print) {
17
- const callNode = path.getValue();
18
- const [receiverNode, _operatorNode, messageNode] = callNode.body;
16
+ const node = path.getValue();
17
+ const [receiverNode, _operatorNode, messageNode] = node.body;
19
18
 
20
19
  const receiverDoc = path.call(print, "body", 0);
21
20
  const operatorDoc = makeCall(path, opts, print);
22
21
 
23
22
  // You can call lambdas with a special syntax that looks like func.(*args).
24
- // In this case, "call" is returned for the 3rd child node.
25
- const messageDoc =
26
- messageNode === "call" ? messageNode : path.call(print, "body", 2);
27
-
28
- // For certain left sides of the call nodes, we want to attach directly to
29
- // the } or end.
30
- if (noIndent.includes(receiverNode.type)) {
31
- return concat([receiverDoc, operatorDoc, messageDoc]);
32
- }
23
+ // In this case, "call" is returned for the 3rd child node. We don't alter
24
+ // call syntax so if `call` is implicit, we don't print it out.
25
+ const messageDoc = messageNode === "call" ? "" : path.call(print, "body", 2);
33
26
 
34
27
  // The right side of the call node, as in everything including the operator
35
28
  // and beyond.
36
- const rightSideDoc = indent(
37
- concat([
38
- receiverNode.comments ? hardline : softline,
39
- operatorDoc,
40
- messageDoc
41
- ])
42
- );
29
+ const rightSideDoc = concat([
30
+ receiverNode.comments ? hardline : softline,
31
+ operatorDoc,
32
+ messageDoc
33
+ ]);
43
34
 
44
35
  // Get a reference to the parent node so we can check if we're inside a chain
45
36
  const parentNode = path.getParentNode();
@@ -47,33 +38,56 @@ function printCall(path, opts, print) {
47
38
  // If our parent node is a chained node then we're not going to group the
48
39
  // right side of the expression, as we want to have a nice multi-line layout.
49
40
  if (chained.includes(parentNode.type)) {
50
- parentNode.chain = (callNode.chain || 0) + 1;
51
- parentNode.breakDoc = (callNode.breakDoc || [receiverDoc]).concat(
52
- rightSideDoc
53
- );
41
+ parentNode.chain = (node.chain || 0) + 1;
42
+ parentNode.callChain = (node.callChain || 0) + 1;
43
+ parentNode.breakDoc = (node.breakDoc || [receiverDoc]).concat(rightSideDoc);
54
44
  }
55
45
 
56
46
  // If we're at the top of a chain, then we're going to print out a nice
57
47
  // multi-line layout if this doesn't break into multiple lines.
58
- if (!chained.includes(parentNode.type) && (callNode.chain || 0) >= 3) {
48
+ if (!chained.includes(parentNode.type) && (node.chain || 0) >= 3) {
59
49
  return ifBreak(
60
- group(concat(callNode.breakDoc.concat(rightSideDoc))),
50
+ group(indent(concat(node.breakDoc.concat(rightSideDoc)))),
61
51
  concat([receiverDoc, group(rightSideDoc)])
62
52
  );
63
53
  }
64
54
 
65
- return group(concat([receiverDoc, group(rightSideDoc)]));
55
+ // For certain left sides of the call nodes, we want to attach directly to
56
+ // the } or end.
57
+ if (noIndent.includes(receiverNode.type)) {
58
+ return concat([receiverDoc, operatorDoc, messageDoc]);
59
+ }
60
+
61
+ return group(concat([receiverDoc, group(indent(rightSideDoc))]));
66
62
  }
67
63
 
68
64
  function printMethodAddArg(path, opts, print) {
69
- const methodAddArgNode = path.getValue();
70
- const argNode = methodAddArgNode.body[1];
65
+ const node = path.getValue();
71
66
 
67
+ const [methodNode, argNode] = node.body;
72
68
  const [methodDoc, argsDoc] = path.map(print, "body");
73
69
 
74
70
  // You can end up here if you have a method with a ? ending, presumably
75
- // because the parser knows that it cannot be a local variable.
71
+ // because the parser knows that it cannot be a local variable. You can also
72
+ // end up here if you are explicitly using an empty set of parentheses.
76
73
  if (argsDoc.length === 0) {
74
+ // If you're using an explicit set of parentheses on something that looks
75
+ // like a constant, then we need to match that in order to maintain valid
76
+ // Ruby. For example, you could do something like Foo(), on which we would
77
+ // need to keep the parentheses to make it look like a method call.
78
+ if (methodNode.type === "fcall" && methodNode.body[0].type === "@const") {
79
+ return concat([methodDoc, "()"]);
80
+ }
81
+
82
+ // If you're using an explicit set parentheses with the special call syntax,
83
+ // then we need to explicitly print out an extra set of parentheses. For
84
+ // example, if you call something like Foo.new.() (implicitly calling the
85
+ // #call method on a new instance of the Foo class), then we have to print
86
+ // out those parentheses, otherwise we'll end up with Foo.new.
87
+ if (methodNode.type === "call" && methodNode.body[2] === "call") {
88
+ return concat([methodDoc, "()"]);
89
+ }
90
+
77
91
  return methodDoc;
78
92
  }
79
93
 
@@ -89,20 +103,30 @@ function printMethodAddArg(path, opts, print) {
89
103
  // If our parent node is a chained node then we're not going to group the
90
104
  // right side of the expression, as we want to have a nice multi-line layout.
91
105
  if (chained.includes(parentNode.type)) {
92
- parentNode.chain = (methodAddArgNode.chain || 0) + 1;
93
- parentNode.breakDoc = (methodAddArgNode.breakDoc || [methodDoc]).concat(
94
- argsDoc
95
- );
106
+ parentNode.chain = (node.chain || 0) + 1;
107
+ parentNode.breakDoc = (node.breakDoc || [methodDoc]).concat(argsDoc);
96
108
  }
97
109
 
98
110
  // If we're at the top of a chain, then we're going to print out a nice
99
111
  // multi-line layout if this doesn't break into multiple lines.
100
- if (
101
- !chained.includes(parentNode.type) &&
102
- (methodAddArgNode.chain || 0) >= 3
103
- ) {
112
+ if (!chained.includes(parentNode.type) && (node.chain || 0) >= 3) {
113
+ // This is pretty specialized behavior. Basically if we're at the top of a
114
+ // chain but we've only had method calls without arguments and now we have
115
+ // arguments, then we're effectively trying to call a method with arguments
116
+ // that is nested under a bunch of stuff. So we group together to first part
117
+ // to make it so just the arguments break. This looks like, for example:
118
+ //
119
+ // config.action_dispatch.rescue_responses.merge!(
120
+ // 'ActiveRecord::ConnectionTimeoutError' => :service_unavailable,
121
+ // 'ActiveRecord::QueryCanceled' => :service_unavailable
122
+ // )
123
+ //
124
+ if (node.callChain === node.chain) {
125
+ return concat([group(indent(concat(node.breakDoc))), group(argsDoc)]);
126
+ }
127
+
104
128
  return ifBreak(
105
- group(concat(methodAddArgNode.breakDoc.concat(argsDoc))),
129
+ group(indent(concat(node.breakDoc.concat(argsDoc)))),
106
130
  concat([methodDoc, argsDoc])
107
131
  );
108
132
  }
@@ -110,15 +134,47 @@ function printMethodAddArg(path, opts, print) {
110
134
  return concat([methodDoc, argsDoc]);
111
135
  }
112
136
 
113
- module.exports = {
114
- call: printCall,
115
- fcall: concatBody,
116
- method_add_arg: printMethodAddArg,
117
- method_add_block: (path, opts, print) => {
118
- const [method, block] = path.getValue().body;
119
- const proc = toProc(path, opts, block);
137
+ // Sorbet type annotations look like the following:
138
+ //
139
+ // {method_add_block
140
+ // [{method_add_arg
141
+ // [{fcall
142
+ // [{@ident "sig"}]},
143
+ // {args []}]},
144
+ // {brace_block [nil, {stmts}]}}]}
145
+ //
146
+ function isSorbetTypeAnnotation(node) {
147
+ const [callNode, blockNode] = node.body;
148
+
149
+ return (
150
+ callNode.type === "method_add_arg" &&
151
+ callNode.body[0].type === "fcall" &&
152
+ callNode.body[0].body[0].body === "sig" &&
153
+ callNode.body[1].type === "args" &&
154
+ callNode.body[1].body.length === 0 &&
155
+ blockNode.type === "brace_block"
156
+ );
157
+ }
120
158
 
121
- if (proc && method.type === "call") {
159
+ function printMethodAddBlock(path, opts, print) {
160
+ const node = path.getValue();
161
+
162
+ const [callNode, blockNode] = node.body;
163
+ const [callDoc, blockDoc] = path.map(print, "body");
164
+
165
+ // Very special handling here for sorbet type annotations. They look like Ruby
166
+ // code, but they're not actually Ruby code, so we're not going to mess with
167
+ // them at all.
168
+ if (isSorbetTypeAnnotation(node)) {
169
+ return opts.originalText.slice(opts.locStart(node), opts.locEnd(node));
170
+ }
171
+
172
+ // Don't bother trying to do any kind of fancy toProc transform if the option
173
+ // is disabled.
174
+ if (opts.rubyToProc) {
175
+ const proc = toProc(path, blockNode);
176
+
177
+ if (proc && callNode.type === "call") {
122
178
  return group(
123
179
  concat([
124
180
  path.call(print, "body", 0),
@@ -132,8 +188,34 @@ module.exports = {
132
188
  if (proc) {
133
189
  return path.call(print, "body", 0);
134
190
  }
191
+ }
135
192
 
136
- return concat(path.map(print, "body"));
137
- },
193
+ // Get a reference to the parent node so we can check if we're inside a chain
194
+ const parentNode = path.getParentNode();
195
+
196
+ // If our parent node is a chained node then we're not going to group the
197
+ // right side of the expression, as we want to have a nice multi-line layout.
198
+ if (chained.includes(parentNode.type)) {
199
+ parentNode.chain = (node.chain || 0) + 1;
200
+ parentNode.breakDoc = (node.breakDoc || [callDoc]).concat(blockDoc);
201
+ }
202
+
203
+ // If we're at the top of a chain, then we're going to print out a nice
204
+ // multi-line layout if this doesn't break into multiple lines.
205
+ if (!chained.includes(parentNode.type) && (node.chain || 0) >= 3) {
206
+ return ifBreak(
207
+ group(indent(concat(node.breakDoc.concat(blockDoc)))),
208
+ concat([callDoc, blockDoc])
209
+ );
210
+ }
211
+
212
+ return concat([callDoc, blockDoc]);
213
+ }
214
+
215
+ module.exports = {
216
+ call: printCall,
217
+ fcall: first,
218
+ method_add_arg: printMethodAddArg,
219
+ method_add_block: printMethodAddBlock,
138
220
  vcall: first
139
221
  };