prettier 1.2.1 → 1.3.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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -1
  3. data/CONTRIBUTING.md +2 -2
  4. data/README.md +21 -91
  5. data/lib/prettier.rb +2 -2
  6. data/node_modules/prettier/index.js +54 -54
  7. data/package.json +3 -4
  8. data/rubocop.yml +26 -0
  9. data/src/haml/embed.js +87 -0
  10. data/src/haml/nodes/comment.js +27 -0
  11. data/src/haml/nodes/doctype.js +34 -0
  12. data/src/haml/nodes/filter.js +16 -0
  13. data/src/haml/nodes/hamlComment.js +21 -0
  14. data/src/haml/nodes/plain.js +6 -0
  15. data/src/haml/nodes/root.js +8 -0
  16. data/src/haml/nodes/script.js +33 -0
  17. data/src/haml/nodes/silentScript.js +59 -0
  18. data/src/haml/nodes/tag.js +193 -0
  19. data/src/haml/parser.js +33 -0
  20. data/src/haml/parser.rb +141 -0
  21. data/src/haml/printer.js +28 -0
  22. data/src/{ruby.js → plugin.js} +25 -4
  23. data/src/prettier.js +1 -0
  24. data/src/rbs/parser.js +51 -0
  25. data/src/rbs/parser.rb +91 -0
  26. data/src/rbs/printer.js +605 -0
  27. data/src/{embed.js → ruby/embed.js} +6 -2
  28. data/src/{nodes.js → ruby/nodes.js} +0 -0
  29. data/src/{nodes → ruby/nodes}/alias.js +1 -1
  30. data/src/{nodes → ruby/nodes}/aref.js +8 -1
  31. data/src/{nodes → ruby/nodes}/args.js +2 -2
  32. data/src/{nodes → ruby/nodes}/arrays.js +2 -3
  33. data/src/{nodes → ruby/nodes}/assign.js +7 -3
  34. data/src/ruby/nodes/blocks.js +90 -0
  35. data/src/{nodes → ruby/nodes}/calls.js +18 -11
  36. data/src/{nodes → ruby/nodes}/case.js +1 -1
  37. data/src/{nodes → ruby/nodes}/class.js +1 -1
  38. data/src/ruby/nodes/commands.js +131 -0
  39. data/src/{nodes → ruby/nodes}/conditionals.js +3 -3
  40. data/src/{nodes → ruby/nodes}/constants.js +2 -2
  41. data/src/{nodes → ruby/nodes}/flow.js +2 -2
  42. data/src/{nodes → ruby/nodes}/hashes.js +32 -10
  43. data/src/{nodes → ruby/nodes}/heredocs.js +2 -2
  44. data/src/ruby/nodes/hooks.js +34 -0
  45. data/src/{nodes → ruby/nodes}/ints.js +0 -0
  46. data/src/{nodes → ruby/nodes}/lambdas.js +2 -2
  47. data/src/{nodes → ruby/nodes}/loops.js +10 -7
  48. data/src/{nodes → ruby/nodes}/massign.js +8 -1
  49. data/src/{nodes → ruby/nodes}/methods.js +10 -9
  50. data/src/{nodes → ruby/nodes}/operators.js +2 -2
  51. data/src/{nodes → ruby/nodes}/params.js +31 -16
  52. data/src/{nodes → ruby/nodes}/patterns.js +17 -6
  53. data/src/{nodes → ruby/nodes}/regexp.js +2 -2
  54. data/src/{nodes → ruby/nodes}/rescue.js +2 -2
  55. data/src/ruby/nodes/return.js +94 -0
  56. data/src/{nodes → ruby/nodes}/statements.js +6 -9
  57. data/src/{nodes → ruby/nodes}/strings.js +27 -36
  58. data/src/{nodes → ruby/nodes}/super.js +2 -2
  59. data/src/{nodes → ruby/nodes}/undef.js +1 -1
  60. data/src/{parser.js → ruby/parser.js} +4 -3
  61. data/src/{parser.rb → ruby/parser.rb} +450 -501
  62. data/src/{printer.js → ruby/printer.js} +33 -1
  63. data/src/{toProc.js → ruby/toProc.js} +4 -8
  64. data/src/utils.js +10 -93
  65. data/src/utils/containsAssignment.js +11 -0
  66. data/src/utils/getTrailingComma.js +5 -0
  67. data/src/utils/hasAncestor.js +17 -0
  68. data/src/utils/literal.js +7 -0
  69. data/src/utils/makeCall.js +14 -0
  70. data/src/utils/noIndent.js +11 -0
  71. data/src/utils/skipAssignIndent.js +10 -0
  72. metadata +65 -41
  73. data/src/nodes/blocks.js +0 -85
  74. data/src/nodes/commands.js +0 -91
  75. data/src/nodes/hooks.js +0 -44
  76. data/src/nodes/return.js +0 -72
@@ -0,0 +1,33 @@
1
+ const { spawnSync } = require("child_process");
2
+ const path = require("path");
3
+
4
+ const parser = path.join(__dirname, "./parser.rb");
5
+
6
+ const parse = (text, _parsers, _opts) => {
7
+ const child = spawnSync("ruby", [parser], { input: text });
8
+
9
+ const error = child.stderr.toString();
10
+ if (error) {
11
+ throw new Error(error);
12
+ }
13
+
14
+ const response = child.stdout.toString();
15
+ return JSON.parse(response);
16
+ };
17
+
18
+ const pragmaPattern = /^\s*-#\s*@(prettier|format)/;
19
+ const hasPragma = (text) => pragmaPattern.test(text);
20
+
21
+ // These functions are just placeholders until we can actually perform this
22
+ // properly. The functions are necessary otherwise the format with cursor
23
+ // functions break.
24
+ const locStart = (_node) => 0;
25
+ const locEnd = (_node) => 0;
26
+
27
+ module.exports = {
28
+ parse,
29
+ astFormat: "haml",
30
+ hasPragma,
31
+ locStart,
32
+ locEnd
33
+ };
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup' if ENV['CI']
4
+ require 'haml'
5
+ require 'json'
6
+ require 'ripper'
7
+
8
+ class Haml::Parser::ParseNode
9
+ class DeepAttributeParser
10
+ def parse(string)
11
+ Haml::AttributeParser.available? ? parse_value(string) : string
12
+ end
13
+
14
+ private
15
+
16
+ def literal(string, level)
17
+ level == 0 ? string : "&#{string}"
18
+ end
19
+
20
+ def parse_value(string, level = 0)
21
+ response = Ripper.sexp(string)
22
+ return literal(string, level) unless response
23
+
24
+ case response[1][0][0]
25
+ when :hash
26
+ hash = Haml::AttributeParser.parse(string)
27
+
28
+ if hash
29
+ # Explicitly not using Enumerable#to_h here to support Ruby 2.5
30
+ hash.each_with_object({}) do |(key, value), response|
31
+ response[key] = parse_value(value, level + 1)
32
+ end
33
+ else
34
+ literal(string, level)
35
+ end
36
+ when :string_literal
37
+ string[1...-1]
38
+ else
39
+ literal(string, level)
40
+ end
41
+ end
42
+ end
43
+
44
+ ESCAPE = /Haml::Helpers.html_escape\(\((.+)\)\)/.freeze
45
+
46
+ # If a node comes in as the plain type but starts with one of the special
47
+ # characters that haml parses, then we need to escape it with a \ when
48
+ # printing. So here we make a regexp pattern to check if the node needs to be
49
+ # escaped.
50
+ special_chars =
51
+ Haml::Parser::SPECIAL_CHARACTERS.map { |char| Regexp.escape(char) }
52
+
53
+ SPECIAL_START = /\A(?:#{special_chars.join('|')})/
54
+
55
+ def as_json
56
+ case type
57
+ when :comment, :doctype, :silent_script
58
+ to_h.tap do |json|
59
+ json.delete(:parent)
60
+ json[:children] = children.map(&:as_json)
61
+ end
62
+ when :filter, :haml_comment
63
+ to_h.tap { |json| json.delete(:parent) }
64
+ when :plain
65
+ to_h.tap do |json|
66
+ json.delete(:parent)
67
+ json[:children] = children.map(&:as_json)
68
+
69
+ text = json[:value][:text]
70
+ json[:value][:text] = "\\#{text}" if text.match?(SPECIAL_START)
71
+ end
72
+ when :root
73
+ to_h.tap { |json| json[:children] = children.map(&:as_json) }
74
+ when :script
75
+ to_h.tap do |json|
76
+ json.delete(:parent)
77
+ json[:children] = children.map(&:as_json)
78
+
79
+ if json[:value][:text].match?(ESCAPE)
80
+ json[:value][:text].gsub!(ESCAPE) { $1 }
81
+ json[:value].merge!(escape_html: 'escape_html', interpolate: true)
82
+ end
83
+ end
84
+ when :tag
85
+ to_h.tap do |json|
86
+ json.delete(:parent)
87
+
88
+ # For some reason this is actually using a symbol to represent a null
89
+ # object ref instead of nil itself, so just replacing it here for
90
+ # simplicity in the printer
91
+ json[:value][:object_ref] = nil if json[:value][:object_ref] == :nil
92
+
93
+ # Get a reference to the dynamic attributes hash
94
+ dynamic_attributes = value[:dynamic_attributes].to_h
95
+
96
+ # If we have any in the old style, then we're going to pass it through
97
+ # the deep attribute parser filter.
98
+ if dynamic_attributes[:old]
99
+ dynamic_attributes[:old] =
100
+ DeepAttributeParser.new.parse(dynamic_attributes[:old])
101
+ end
102
+
103
+ json.merge!(
104
+ children: children.map(&:as_json),
105
+ value: value.merge(dynamic_attributes: dynamic_attributes)
106
+ )
107
+ end
108
+ else
109
+ raise ArgumentError, "Unsupported type: #{type}"
110
+ end
111
+ end
112
+ end
113
+
114
+ module Prettier
115
+ class HAMLParser
116
+ def self.parse(source)
117
+ Haml::Parser.new({}).call(source).as_json
118
+ rescue StandardError
119
+ false
120
+ end
121
+ end
122
+ end
123
+
124
+ # If this is the main file we're executing, then most likely this is being
125
+ # executed from the haml.js spawn. In that case, read the ruby source from
126
+ # stdin and report back the AST over stdout.
127
+ if $0 == __FILE__
128
+ response = Prettier::HAMLParser.parse($stdin.read)
129
+
130
+ if !response
131
+ warn(
132
+ '@prettier/plugin-ruby encountered an error when attempting to parse ' \
133
+ 'the HAML source. This usually means there was a syntax error in the ' \
134
+ 'file in question.'
135
+ )
136
+
137
+ exit 1
138
+ end
139
+
140
+ puts JSON.fast_generate(response)
141
+ end
@@ -0,0 +1,28 @@
1
+ const embed = require("./embed");
2
+ const nodes = {
3
+ comment: require("./nodes/comment"),
4
+ doctype: require("./nodes/doctype"),
5
+ filter: require("./nodes/filter"),
6
+ haml_comment: require("./nodes/hamlComment"),
7
+ plain: require("./nodes/plain"),
8
+ root: require("./nodes/root"),
9
+ script: require("./nodes/script"),
10
+ silent_script: require("./nodes/silentScript"),
11
+ tag: require("./nodes/tag")
12
+ };
13
+
14
+ const genericPrint = (path, opts, print) => {
15
+ const { type } = path.getValue();
16
+
17
+ /* istanbul ignore next */
18
+ if (!(type in nodes)) {
19
+ throw new Error(`Unsupported node encountered: ${type}`);
20
+ }
21
+
22
+ return nodes[type](path, opts, print);
23
+ };
24
+
25
+ module.exports = {
26
+ embed,
27
+ print: genericPrint
28
+ };
@@ -1,5 +1,11 @@
1
- const printer = require("./printer");
2
- const parser = require("./parser");
1
+ const rubyPrinter = require("./ruby/printer");
2
+ const rubyParser = require("./ruby/parser");
3
+
4
+ const rbsPrinter = require("./rbs/printer");
5
+ const rbsParser = require("./rbs/parser");
6
+
7
+ const hamlPrinter = require("./haml/printer");
8
+ const hamlParser = require("./haml/parser");
3
9
 
4
10
  /*
5
11
  * metadata mostly pulled from linguist and rubocop:
@@ -67,13 +73,28 @@ module.exports = {
67
73
  interpreters: ["jruby", "macruby", "rake", "rbx", "ruby"],
68
74
  linguistLanguageId: 326,
69
75
  vscodeLanguageIds: ["ruby"]
76
+ },
77
+ {
78
+ name: "RBS",
79
+ parsers: ["rbs"],
80
+ extensions: [".rbs"]
81
+ },
82
+ {
83
+ name: "HAML",
84
+ parsers: ["haml"],
85
+ extensions: [".haml"],
86
+ vscodeLanguageIds: ["haml"]
70
87
  }
71
88
  ],
72
89
  parsers: {
73
- ruby: parser
90
+ ruby: rubyParser,
91
+ rbs: rbsParser,
92
+ haml: hamlParser
74
93
  },
75
94
  printers: {
76
- ruby: printer
95
+ ruby: rubyPrinter,
96
+ rbs: rbsPrinter,
97
+ haml: hamlPrinter
77
98
  },
78
99
  options: {
79
100
  rubyArrayLiteral: {
@@ -1,6 +1,7 @@
1
1
  // If `RBPRETTIER` is set, then this is being run from the `Prettier::run` ruby
2
2
  // method. In that case, we need to pull `prettier` from the node_modules
3
3
  // directly, as it's been shipped with the gem.
4
+ /* istanbul ignore next */
4
5
  const source = process.env.RBPRETTIER ? "../node_modules/prettier" : "prettier";
5
6
 
6
7
  const prettier = require(source);
@@ -0,0 +1,51 @@
1
+ const { spawnSync } = require("child_process");
2
+ const path = require("path");
3
+
4
+ // This function is responsible for taking an input string of text and returning
5
+ // to prettier a JavaScript object that is the equivalent AST that represents
6
+ // the code stored in that string. We accomplish this by spawning a new Ruby
7
+ // process of parser.rb and reading JSON off STDOUT.
8
+ function parse(text, _parsers, _opts) {
9
+ const child = spawnSync("ruby", [path.join(__dirname, "./parser.rb")], {
10
+ input: text,
11
+ maxBuffer: 15 * 1024 * 1024 // 15MB
12
+ });
13
+
14
+ const error = child.stderr.toString();
15
+ if (error) {
16
+ throw new Error(error);
17
+ }
18
+
19
+ const response = child.stdout.toString();
20
+ return JSON.parse(response);
21
+ }
22
+
23
+ const pragmaPattern = /#\s*@(prettier|format)/;
24
+
25
+ // This function handles checking whether or not the source string has the
26
+ // pragma for prettier. This is an optional workflow for incremental adoption.
27
+ function hasPragma(text) {
28
+ return pragmaPattern.test(text);
29
+ }
30
+
31
+ // This function is critical for comments and cursor support, and is responsible
32
+ // for returning the index of the character within the source string that is the
33
+ // beginning of the given node.
34
+ function locStart(node) {
35
+ return (node.location || node.type.location).start_pos;
36
+ }
37
+
38
+ // This function is critical for comments and cursor support, and is responsible
39
+ // for returning the index of the character within the source string that is the
40
+ // ending of the given node.
41
+ function locEnd(node) {
42
+ return (node.location || node.type.location).end_pos;
43
+ }
44
+
45
+ module.exports = {
46
+ parse,
47
+ astFormat: "rbs",
48
+ hasPragma,
49
+ locStart,
50
+ locEnd
51
+ };
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup' if ENV['CI']
4
+ require 'json'
5
+ require 'rbs'
6
+
7
+ # Monkey-patch this so that we can get the character positions.
8
+ class RBS::Location
9
+ def to_json(*args)
10
+ {
11
+ start: { line: start_line, column: start_column },
12
+ end: { line: end_line, column: end_column },
13
+ start_pos: start_pos,
14
+ end_pos: end_pos
15
+ }.to_json(*args)
16
+ end
17
+ end
18
+
19
+ class RBS::Types::Function::Param
20
+ def to_json(*a)
21
+ escaped = name && /\A#{RBS::Parser::KEYWORDS_RE}\z/.match?(name)
22
+ { type: type, name: name, escaped: escaped }.to_json(*a)
23
+ end
24
+ end
25
+
26
+ # Monkey-patch this so that we get the name field in the serialized JSON, as
27
+ # well as information about whether or not we need to escape it.
28
+ class RBS::AST::Members::MethodDefinition
29
+ def to_json(*a)
30
+ {
31
+ member: :method_definition,
32
+ name: name,
33
+ kind: kind,
34
+ types: types,
35
+ annotations: annotations,
36
+ location: location,
37
+ comment: comment,
38
+ overload: overload
39
+ }.to_json(*a)
40
+ end
41
+ end
42
+
43
+ # Monkey-patch this so that we get the information we need about how to join the
44
+ # key-value pairs of the record.
45
+ class RBS::Types::Record
46
+ def to_json(*a)
47
+ fields_extra = {}
48
+
49
+ # Explicitly not using Enumerable#to_h here to support Ruby 2.5
50
+ fields.each do |key, type|
51
+ if key.is_a?(Symbol) && key.match?(/\A[A-Za-z_][A-Za-z_]*\z/) &&
52
+ !key.match?(RBS::Parser::KEYWORDS_RE)
53
+ fields_extra[key] = { type: type, joiner: :label }
54
+ else
55
+ fields_extra[key.inspect] = { type: type, joiner: :rocket }
56
+ end
57
+ end
58
+
59
+ { class: :record, fields: fields_extra, location: location }.to_json(*a)
60
+ end
61
+ end
62
+
63
+ module Prettier
64
+ class RBSParser
65
+ def self.parse(text)
66
+ { declarations: RBS::Parser.parse_signature(text) }
67
+ rescue StandardError
68
+ false
69
+ end
70
+ end
71
+ end
72
+
73
+ # If this is the main file we're executing, then most likely this is being
74
+ # executed from the parser.js spawn. In that case, read the rbs source from
75
+ # stdin and report back the AST over stdout.
76
+
77
+ if $0 == __FILE__
78
+ response = Prettier::RBSParser.parse($stdin.read)
79
+
80
+ if !response
81
+ warn(
82
+ '@prettier/plugin-ruby encountered an error when attempting to parse ' \
83
+ 'the RBS source. This usually means there was a syntax error in the ' \
84
+ 'file in question.'
85
+ )
86
+
87
+ exit 1
88
+ end
89
+
90
+ puts JSON.fast_generate(response)
91
+ end
@@ -0,0 +1,605 @@
1
+ const {
2
+ concat,
3
+ group,
4
+ hardline,
5
+ indent,
6
+ makeString,
7
+ join,
8
+ line,
9
+ softline
10
+ } = require("../prettier");
11
+
12
+ // For some lists of entities in the AST, the parser returns them as an unsorted
13
+ // object (presumably because Ruby hashes have implicit ordering). We do not
14
+ // have that in JavaScript, so here we sort each object by its position in the
15
+ // source string.
16
+ function getSortedKeys(object) {
17
+ return Object.keys(object).sort(
18
+ (left, right) =>
19
+ object[left].type.location.start_pos -
20
+ object[right].type.location.start_pos
21
+ );
22
+ }
23
+
24
+ // In some cases, we want to just defer to whatever was in the source.
25
+ function getSource(node, opts) {
26
+ return opts.originalText.slice(
27
+ node.location.start_pos,
28
+ node.location.end_pos
29
+ );
30
+ }
31
+
32
+ // This is the generic node print function, used to convert any node in the AST
33
+ // into its equivalent Doc representation.
34
+ function printNode(path, opts, print) {
35
+ const node = path.getValue();
36
+ let doc = null;
37
+
38
+ if (node.declarations) {
39
+ return printRoot();
40
+ }
41
+
42
+ /* istanbul ignore else */
43
+ if (node.declaration) {
44
+ switch (node.declaration) {
45
+ case "alias":
46
+ doc = printTypeAlias();
47
+ break;
48
+ case "class":
49
+ doc = printClass();
50
+ break;
51
+ case "constant":
52
+ case "global":
53
+ doc = printConstant();
54
+ break;
55
+ case "interface":
56
+ doc = printInterface();
57
+ break;
58
+ case "module":
59
+ doc = printModule();
60
+ break;
61
+ /* istanbul ignore next */
62
+ default:
63
+ throw new Error(`unknown declaration: ${node.declaration}`);
64
+ }
65
+ } else if (node.member) {
66
+ switch (node.member) {
67
+ case "alias":
68
+ doc = printAlias();
69
+ break;
70
+ case "attr_accessor":
71
+ case "attr_reader":
72
+ case "attr_writer":
73
+ doc = printAttr();
74
+ break;
75
+ case "class_variable":
76
+ case "instance_variable":
77
+ doc = printVariable();
78
+ break;
79
+ case "class_instance_variable":
80
+ doc = concat(["self.", printVariable()]);
81
+ break;
82
+ case "include":
83
+ case "extend":
84
+ case "prepend":
85
+ doc = printMixin();
86
+ break;
87
+ case "public":
88
+ case "private":
89
+ doc = node.member;
90
+ break;
91
+ case "method_definition":
92
+ doc = printMethodDefinition();
93
+ break;
94
+ /* istanbul ignore next */
95
+ default:
96
+ throw new Error(`unknown member: ${node.member}`);
97
+ }
98
+ } else {
99
+ const ast = JSON.stringify(node, null, 2);
100
+ throw new Error(`Unsupported node encountered:\n${ast}`);
101
+ }
102
+
103
+ // Certain nodes can't have annotations at all
104
+ if (node.annotations && node.annotations.length > 0) {
105
+ doc = concat([printAnnotations(), hardline, doc]);
106
+ }
107
+
108
+ if (node.comment) {
109
+ doc = concat([printComment(), hardline, doc]);
110
+ }
111
+
112
+ return doc;
113
+
114
+ // Prints out a string in the source, which looks like:
115
+ // 'foo'
116
+ function printString(node) {
117
+ // We're going to go straight to the source here, as if we don't then we're
118
+ // going to end up with the result of String#inspect, which does weird
119
+ // things to escape sequences.
120
+ const value = getSource(node, opts);
121
+
122
+ // Get the quote that was used in the source and the quote that we want to
123
+ // be using.
124
+ const originalQuote = value[0];
125
+ const preferredQuote = opts.rubySingleQuote ? "'" : '"';
126
+
127
+ // Determine if we're allowed to change the quote based on whether or not
128
+ // there is an escape sequence in the source string.
129
+ const quote = node.literal.includes("\\") ? originalQuote : preferredQuote;
130
+
131
+ return makeString(value.slice(1, -1), quote, false);
132
+ }
133
+
134
+ // Certain nodes are names with optional arguments attached, as in Array[A].
135
+ // We handle all of that printing centralized here.
136
+ function printNameAndArgs(path) {
137
+ const node = path.getValue();
138
+
139
+ if (node.args.length === 0) {
140
+ return node.name;
141
+ }
142
+
143
+ return group(
144
+ concat([node.name, "[", join(", ", path.map(printType, "args")), "]"])
145
+ );
146
+ }
147
+
148
+ // This is the big function that prints out any individual type, which can
149
+ // look like all kinds of things, listed in the case statement below.
150
+ function printType(path) {
151
+ const node = path.getValue();
152
+
153
+ switch (node.class) {
154
+ case "literal":
155
+ if (node.literal[0] === '"') {
156
+ return printString(node);
157
+ }
158
+ return node.literal;
159
+ case "optional":
160
+ return concat([path.call(printType, "type"), "?"]);
161
+ case "tuple":
162
+ // If we don't have any sub types, we explicitly need the space in between
163
+ // the brackets to not confuse the parser.
164
+ if (node.types.length === 0) {
165
+ return "[ ]";
166
+ }
167
+
168
+ return group(
169
+ concat(["[", join(", ", path.map(printType, "types")), "]"])
170
+ );
171
+ case "union": {
172
+ const doc = group(
173
+ join(concat([line, "| "]), path.map(printType, "types"))
174
+ );
175
+
176
+ const parentType = path.getParentNode().class;
177
+ return parentType === "intersection" ? concat(["(", doc, ")"]) : doc;
178
+ }
179
+ case "intersection":
180
+ return group(join(concat([line, "& "]), path.map(printType, "types")));
181
+ case "class_singleton":
182
+ return concat(["singleton(", node.name, ")"]);
183
+ case "proc":
184
+ return concat(["^", printMethodSignature(path)]);
185
+ case "record": {
186
+ const parts = [];
187
+
188
+ getSortedKeys(node.fields).forEach((field) => {
189
+ const fieldParts = [];
190
+
191
+ if (node.fields[field].joiner === "rocket") {
192
+ fieldParts.push(`${field} => `);
193
+ } else {
194
+ fieldParts.push(`${field}: `);
195
+ }
196
+
197
+ fieldParts.push(path.call(printType, "fields", field, "type"));
198
+ parts.push(concat(fieldParts));
199
+ });
200
+
201
+ return group(
202
+ concat([
203
+ "{",
204
+ indent(concat([line, join(concat([",", line]), parts)])),
205
+ line,
206
+ "}"
207
+ ])
208
+ );
209
+ }
210
+ case "class_instance":
211
+ case "interface":
212
+ return printNameAndArgs(path);
213
+ case "alias":
214
+ case "variable":
215
+ return node.name;
216
+ case "bool":
217
+ case "bot":
218
+ case "class":
219
+ case "instance":
220
+ case "nil":
221
+ case "self":
222
+ case "top":
223
+ case "untyped":
224
+ case "void":
225
+ return node.class;
226
+ /* istanbul ignore next */
227
+ default:
228
+ throw new Error(`unknown type: ${node.class}`);
229
+ }
230
+ }
231
+
232
+ // Prints out the root of the tree, which includes zero or more declarations.
233
+ function printRoot() {
234
+ return concat([
235
+ join(concat([hardline, hardline]), path.map(print, "declarations")),
236
+ hardline
237
+ ]);
238
+ }
239
+
240
+ // Prints out the members of a class, module, or interface.
241
+ function printMembers() {
242
+ let lastLine = null;
243
+ const docs = [];
244
+
245
+ path.each((memberPath) => {
246
+ const memberNode = memberPath.getValue();
247
+
248
+ if (lastLine !== null && memberNode.location.start.line - lastLine >= 2) {
249
+ docs.push(concat([hardline, hardline]));
250
+ } else {
251
+ docs.push(hardline);
252
+ }
253
+
254
+ docs.push(print(memberPath));
255
+ lastLine = memberNode.location.end.line;
256
+ }, "members");
257
+
258
+ return concat(docs);
259
+ }
260
+
261
+ // Prints out a type alias, which is a declaration that looks like:
262
+ // type foo = String
263
+ function printTypeAlias() {
264
+ return group(
265
+ concat([
266
+ "type ",
267
+ node.name,
268
+ " =",
269
+ indent(group(concat([line, path.call(printType, "type")])))
270
+ ])
271
+ );
272
+ }
273
+
274
+ // Prints out the name of a class, interface, or module declaration.
275
+ // Additionally loops through each type parameter if there are any and print
276
+ // them out joined by commas. Checks for validation and variance.
277
+ function printNameAndTypeParams() {
278
+ if (node.type_params.params.length === 0) {
279
+ return node.name;
280
+ }
281
+
282
+ const docs = path.map(
283
+ (paramPath) => {
284
+ const node = paramPath.getValue();
285
+ const parts = [];
286
+
287
+ if (node.skip_validation) {
288
+ parts.push("unchecked");
289
+ }
290
+
291
+ if (node.variance === "covariant") {
292
+ parts.push("out");
293
+ } else if (node.variance === "contravariant") {
294
+ parts.push("in");
295
+ }
296
+
297
+ return join(" ", parts.concat(node.name));
298
+ },
299
+ "type_params",
300
+ "params"
301
+ );
302
+
303
+ return concat([node.name, "[", join(", ", docs), "]"]);
304
+ }
305
+
306
+ // Prints out a class declarations, which looks like:
307
+ // class Foo end
308
+ function printClass() {
309
+ const parts = ["class ", printNameAndTypeParams()];
310
+
311
+ if (node.super_class) {
312
+ parts.push(" < ", path.call(printNameAndArgs, "super_class"));
313
+ }
314
+
315
+ parts.push(indent(printMembers()), hardline, "end");
316
+
317
+ return group(concat(parts));
318
+ }
319
+
320
+ // Prints out a constant or a global declaration, which looks like:
321
+ // Foo: String
322
+ // $foo: String
323
+ function printConstant() {
324
+ return group(concat([node.name, ": ", path.call(printType, "type")]));
325
+ }
326
+
327
+ // Prints out an interface declaration, which looks like:
328
+ // interface _Foo end
329
+ function printInterface() {
330
+ return group(
331
+ concat([
332
+ "interface ",
333
+ printNameAndTypeParams(),
334
+ indent(printMembers()),
335
+ hardline,
336
+ "end"
337
+ ])
338
+ );
339
+ }
340
+
341
+ // Prints out a module declaration, which looks like:
342
+ // module Foo end
343
+ function printModule() {
344
+ const parts = ["module ", printNameAndTypeParams()];
345
+
346
+ if (node.self_types.length > 0) {
347
+ parts.push(" : ", join(", ", path.map(printNameAndArgs, "self_types")));
348
+ }
349
+
350
+ parts.push(indent(printMembers()), hardline, "end");
351
+
352
+ return group(concat(parts));
353
+ }
354
+
355
+ // Prints out an alias within a declaration, which looks like:
356
+ // alias foo bar
357
+ // alias self.foo self.bar
358
+ function printAlias() {
359
+ if (node.kind === "singleton") {
360
+ return concat(["alias self.", node.new_name, " self.", node.old_name]);
361
+ }
362
+
363
+ return concat(["alias ", node.new_name, " ", node.old_name]);
364
+ }
365
+
366
+ // Prints out an attr_* meta method, which looks like:
367
+ // attr_accessor foo
368
+ // attr_reader self.foo()
369
+ // attr_writer self.foo(@bar): String
370
+ function printAttr() {
371
+ const parts = [node.member, " "];
372
+
373
+ if (node.kind === "singleton") {
374
+ parts.push("self.");
375
+ }
376
+
377
+ parts.push(node.name);
378
+
379
+ if (node.ivar_name === false) {
380
+ parts.push("()");
381
+ } else if (node.ivar_name) {
382
+ parts.push("(", node.ivar_name, ")");
383
+ }
384
+
385
+ parts.push(": ", path.call(printType, "type"));
386
+
387
+ return group(concat(parts));
388
+ }
389
+
390
+ // Prints out a variable member, which looks like:
391
+ // @foo: String
392
+ // self.@foo: String
393
+ // @@foo: String
394
+ function printVariable() {
395
+ return group(concat([node.name, ": ", path.call(printType, "type")]));
396
+ }
397
+
398
+ // Prints out a mixin, which looks like:
399
+ // include Foo
400
+ // prepend Foo
401
+ // extend Foo
402
+ function printMixin() {
403
+ return group(concat([node.member, " ", printNameAndArgs(path)]));
404
+ }
405
+
406
+ // Returns an array of printed parameters so that the calling function can
407
+ // join them together in whatever way.
408
+ function printMethodParams(path) {
409
+ const node = path.getValue();
410
+ let parts = [];
411
+
412
+ // required positionals, as in (A)
413
+ parts = parts.concat(path.map(printMethodParam, "required_positionals"));
414
+
415
+ // optional positionals, as in (?A)
416
+ parts = parts.concat(
417
+ path.map(
418
+ (paramPath) => concat(["?", printMethodParam(paramPath)]),
419
+ "optional_positionals"
420
+ )
421
+ );
422
+
423
+ // rest positional, as in (*A)
424
+ if (node.rest_positionals) {
425
+ parts.push(
426
+ concat(["*", path.call(printMethodParam, "rest_positionals")])
427
+ );
428
+ }
429
+
430
+ // trailing positionals are required positionals after a rest
431
+ parts = parts.concat(path.map(printMethodParam, "trailing_positionals"));
432
+
433
+ // required keywords, as in (a: A)
434
+ getSortedKeys(node.required_keywords).forEach((name) => {
435
+ parts.push(
436
+ concat([
437
+ name,
438
+ ": ",
439
+ path.call(printMethodParam, "required_keywords", name)
440
+ ])
441
+ );
442
+ });
443
+
444
+ // optional keywords, as in (?a: A)
445
+ getSortedKeys(node.optional_keywords).forEach((name) => {
446
+ parts.push(
447
+ concat([
448
+ "?",
449
+ name,
450
+ ": ",
451
+ path.call(printMethodParam, "optional_keywords", name)
452
+ ])
453
+ );
454
+ });
455
+
456
+ // rest keyword, as in (**A)
457
+ if (node.rest_keywords) {
458
+ parts.push(concat(["**", path.call(printMethodParam, "rest_keywords")]));
459
+ }
460
+
461
+ return parts;
462
+
463
+ // Prints out a method parameter at a given path. Handles printing out the
464
+ // name if there is one (and whether or not it's escaped).
465
+ function printMethodParam(path) {
466
+ const node = path.getValue();
467
+ const parts = [path.call(printType, "type")];
468
+
469
+ if (node.name) {
470
+ parts.push(" ");
471
+
472
+ if (node.escaped) {
473
+ parts.push("`", node.name, "`");
474
+ } else {
475
+ parts.push(node.name);
476
+ }
477
+ }
478
+
479
+ return concat(parts);
480
+ }
481
+ }
482
+
483
+ // Prints out a specific method signature, which looks like:
484
+ // (T t) -> void
485
+ function printMethodSignature(path) {
486
+ const node = path.getValue();
487
+ const parts = [];
488
+
489
+ // We won't have a type_params key if we're printing a block
490
+ if (node.type_params && node.type_params.length > 0) {
491
+ parts.push("[", join(", ", node.type_params), "] ");
492
+ }
493
+
494
+ let params = path.call(printMethodParams, "type");
495
+
496
+ if (params.length > 0) {
497
+ parts.push(
498
+ "(",
499
+ indent(concat([softline, join(concat([",", line]), params)])),
500
+ softline,
501
+ ") "
502
+ );
503
+ }
504
+
505
+ if (node.block) {
506
+ if (!node.block.required) {
507
+ parts.push("?");
508
+ }
509
+
510
+ parts.push(
511
+ "{",
512
+ indent(concat([line, path.call(printMethodSignature, "block")])),
513
+ line,
514
+ "} "
515
+ );
516
+ }
517
+
518
+ parts.push("-> ", path.call(printType, "type", "return_type"));
519
+
520
+ return group(concat(parts));
521
+ }
522
+
523
+ // Prints out a method definition, which looks like:
524
+ // def t: (T t) -> void
525
+ function printMethodDefinition() {
526
+ let typeDocs = path.map(printMethodSignature, "types");
527
+
528
+ if (node.overload) {
529
+ typeDocs.push("...");
530
+ }
531
+
532
+ if (typeDocs.length === 1) {
533
+ typeDocs = concat([" ", typeDocs[0]]);
534
+ } else {
535
+ typeDocs = indent(
536
+ group(concat([line, join(concat([line, "| "]), typeDocs)]))
537
+ );
538
+ }
539
+
540
+ const parts = ["def "];
541
+
542
+ if (node.kind === "singleton") {
543
+ parts.push("self.");
544
+ } else if (node.kind === "singleton_instance") {
545
+ parts.push("self?.");
546
+ }
547
+
548
+ const escaped = isMethodNameEscaped();
549
+ parts.push(escaped ? `\`${node.name}\`` : node.name, ":", typeDocs);
550
+
551
+ return group(concat(parts));
552
+
553
+ // Determine if a method name is escaped in the original source.
554
+ function isMethodNameEscaped() {
555
+ const pos = node.location.start_pos + 4;
556
+ const name = opts.originalText.slice(pos, pos + 2).trimStart();
557
+
558
+ return name[0] === "`" && name[1] !== ":";
559
+ }
560
+ }
561
+
562
+ // An annotation can be attached to most kinds of nodes, and should be printed
563
+ // using %a{}.
564
+ function printAnnotations() {
565
+ return join(hardline, path.map(printAnnotation, "annotations"));
566
+
567
+ function printAnnotation(path) {
568
+ const node = path.getValue();
569
+
570
+ // If there are already braces inside the annotation, then we're just
571
+ // going to print out the original string to avoid having to escape
572
+ // anything.
573
+ if (/[{}]/.test(node.string)) {
574
+ return getSource(node, opts);
575
+ }
576
+
577
+ return concat(["%a{", node.string, "}"]);
578
+ }
579
+ }
580
+
581
+ // Comments come in as one whole string, so here we split it up into multiple
582
+ // lines and then prefix it with the pound sign.
583
+ function printComment() {
584
+ const lines = node.comment.string.slice(0, -1).split("\n");
585
+
586
+ return join(
587
+ hardline,
588
+ lines.map((segment) => `# ${segment}`)
589
+ );
590
+ }
591
+ }
592
+
593
+ // This is an escape-hatch to ignore nodes in the tree. If you have a comment
594
+ // that includes this pattern, then the entire node will be ignored and just the
595
+ // original source will be printed out.
596
+ function hasPrettierIgnore(path) {
597
+ const node = path.getValue();
598
+
599
+ return node.comment && node.comment.string.includes("prettier-ignore");
600
+ }
601
+
602
+ module.exports = {
603
+ print: printNode,
604
+ hasPrettierIgnore
605
+ };