prettier 1.3.0 → 1.5.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -73,7 +73,7 @@ function printCommand(path, opts, print) {
73
73
  } else if (hasDef(path.getValue())) {
74
74
  breakArgs = joinedArgs;
75
75
  } else {
76
- breakArgs = align(command.length + 1, joinedArgs);
76
+ breakArgs = align(docLength(command) + 1, joinedArgs);
77
77
  }
78
78
 
79
79
  return group(
@@ -219,7 +219,7 @@ const printConditional = (keyword) => (path, { rubyModifier }, print) => {
219
219
 
220
220
  // If the body of the conditional is empty, then we explicitly have to use the
221
221
  // block form.
222
- if (isEmptyStmts(statements) && !statements.body[0].comments) {
222
+ if (isEmptyStmts(statements)) {
223
223
  return concat([
224
224
  `${keyword} `,
225
225
  align(keyword.length + 1, path.call(print, "body", 0)),
@@ -6,6 +6,7 @@ const {
6
6
  join,
7
7
  line
8
8
  } = require("../../prettier");
9
+
9
10
  const {
10
11
  getTrailingComma,
11
12
  printEmptyCollection,
@@ -89,6 +90,13 @@ function printAssocNew(path, opts, print) {
89
90
  const parts = [path.call((keyPath) => keyPrinter(keyPath, print), "body", 0)];
90
91
  const valueDoc = path.call(print, "body", 1);
91
92
 
93
+ // If we're printing a child hash then we want it to break along with its
94
+ // parent hash, so we don't group the parts.
95
+ if (valueNode.type === "hash") {
96
+ parts.push(" ", valueDoc);
97
+ return concat(parts);
98
+ }
99
+
92
100
  if (!skipAssignIndent(valueNode) || keyNode.comments) {
93
101
  parts.push(indent(concat([line, valueDoc])));
94
102
  } else {
@@ -125,20 +133,26 @@ function printHash(path, opts, print) {
125
133
  return printEmptyCollection(path, opts, "{", "}");
126
134
  }
127
135
 
128
- return group(
129
- concat([
130
- "{",
131
- indent(
132
- concat([
133
- line,
134
- path.call(print, "body", 0),
135
- getTrailingComma(opts) ? ifBreak(",", "") : ""
136
- ])
137
- ),
138
- line,
139
- "}"
140
- ])
141
- );
136
+ let hashDoc = concat([
137
+ "{",
138
+ indent(
139
+ concat([
140
+ line,
141
+ path.call(print, "body", 0),
142
+ getTrailingComma(opts) ? ifBreak(",", "") : ""
143
+ ])
144
+ ),
145
+ line,
146
+ "}"
147
+ ]);
148
+
149
+ // If we're inside another hash, then we don't want to group our contents
150
+ // because we want this hash to break along with its parent hash.
151
+ if (path.getParentNode().type === "assoc_new") {
152
+ return hashDoc;
153
+ }
154
+
155
+ return group(hashDoc);
142
156
  }
143
157
 
144
158
  module.exports = {
@@ -1,5 +1,5 @@
1
1
  const { concat, group, lineSuffix, join } = require("../../prettier");
2
- const { literalLineNoBreak } = require("../../utils");
2
+ const { literallineWithoutBreakParent } = require("../../utils");
3
3
 
4
4
  function printHeredoc(path, opts, print) {
5
5
  const { body, ending } = path.getValue();
@@ -11,7 +11,7 @@ function printHeredoc(path, opts, print) {
11
11
  }
12
12
 
13
13
  // In this case, the part of the string is just regular string content
14
- return join(literalLineNoBreak, part.body.split("\n"));
14
+ return join(literallineWithoutBreakParent, part.body.split("\n"));
15
15
  });
16
16
 
17
17
  // We use a literalline break because matching indentation is required
@@ -23,7 +23,9 @@ function printHeredoc(path, opts, print) {
23
23
  concat([
24
24
  path.call(print, "beging"),
25
25
  lineSuffix(
26
- group(concat([literalLineNoBreak].concat(parts).concat(ending)))
26
+ group(
27
+ concat([literallineWithoutBreakParent].concat(parts).concat(ending))
28
+ )
27
29
  )
28
30
  ])
29
31
  );
@@ -10,7 +10,7 @@ const {
10
10
  softline
11
11
  } = require("../../prettier");
12
12
 
13
- const { containsAssignment } = require("../../utils");
13
+ const { containsAssignment, isEmptyStmts } = require("../../utils");
14
14
  const inlineEnsureParens = require("../../utils/inlineEnsureParens");
15
15
 
16
16
  function printLoop(keyword, modifier) {
@@ -19,17 +19,11 @@ function printLoop(keyword, modifier) {
19
19
 
20
20
  // If the only statement inside this while loop is a void statement, then we
21
21
  // can shorten to just displaying the predicate and then a semicolon.
22
- if (
23
- stmts.body.length === 1 &&
24
- stmts.body[0].type === "void_stmt" &&
25
- !stmts.body[0].comments
26
- ) {
22
+ if (isEmptyStmts(stmts)) {
27
23
  return group(
28
24
  concat([
29
- keyword,
30
- " ",
31
- path.call(print, "body", 0),
32
- ifBreak(softline, "; "),
25
+ group(concat([keyword, " ", path.call(print, "body", 0)])),
26
+ hardline,
33
27
  "end"
34
28
  ])
35
29
  );
@@ -1,8 +1,9 @@
1
1
  const { concat, group, hardline, indent, line } = require("../../prettier");
2
+ const { isEmptyBodyStmt } = require("../../utils");
2
3
 
3
4
  function printMethod(offset) {
4
5
  return function printMethodWithOffset(path, opts, print) {
5
- const [_name, params, body] = path.getValue().body.slice(offset);
6
+ const [_name, params, bodystmt] = path.getValue().body.slice(offset);
6
7
  const declaration = ["def "];
7
8
 
8
9
  // In this case, we're printing a method that's defined as a singleton, so
@@ -24,16 +25,8 @@ function printMethod(offset) {
24
25
  parens ? ")" : ""
25
26
  );
26
27
 
27
- // If the body is empty, we can replace with a ;
28
- const stmts = body.body[0].body;
29
-
30
- if (
31
- !body.body.slice(1).some((node) => node) &&
32
- stmts.length === 1 &&
33
- stmts[0].type === "void_stmt" &&
34
- !stmts[0].comments
35
- ) {
36
- return group(concat(declaration.concat(["; end"])));
28
+ if (isEmptyBodyStmt(bodystmt)) {
29
+ return group(concat(declaration.concat("; end")));
37
30
  }
38
31
 
39
32
  return group(
@@ -26,28 +26,10 @@ function printEnsure(path, opts, print) {
26
26
  }
27
27
 
28
28
  function printRescue(path, opts, print) {
29
- const [exception, variable, _stmts, addition] = path.getValue().body;
30
29
  const parts = ["rescue"];
31
30
 
32
- if (exception || variable) {
33
- if (exception) {
34
- if (Array.isArray(exception)) {
35
- // In this case, it's actually only the one exception (it's an array
36
- // of length 1).
37
- parts.push(" ", path.call(print, "body", 0, 0));
38
- } else {
39
- // Here we have multiple exceptions from which we're rescuing, so we
40
- // need to align them and join them together.
41
- const joiner = concat([",", line]);
42
- const exceptions = group(join(joiner, path.call(print, "body", 0)));
43
-
44
- parts.push(" ", align("rescue ".length, exceptions));
45
- }
46
- }
47
-
48
- if (variable) {
49
- parts.push(" => ", path.call(print, "body", 1));
50
- }
31
+ if (path.getValue().body[0]) {
32
+ parts.push(align("rescue ".length, path.call(print, "body", 0)));
51
33
  } else {
52
34
  // If you don't specify an error to rescue in a `begin/rescue` block, then
53
35
  // implicitly you're rescuing from `StandardError`. In this case, we're
@@ -55,16 +37,40 @@ function printRescue(path, opts, print) {
55
37
  parts.push(" StandardError");
56
38
  }
57
39
 
58
- const rescueBody = path.call(print, "body", 2);
40
+ const bodystmt = path.call(print, "body", 1);
59
41
 
60
- if (rescueBody.parts.length > 0) {
61
- parts.push(indent(concat([hardline, rescueBody])));
42
+ if (bodystmt.parts.length > 0) {
43
+ parts.push(indent(concat([hardline, bodystmt])));
62
44
  }
63
45
 
64
46
  // This is the next clause on the `begin` statement, either another
65
47
  // `rescue`, and `ensure`, or an `else` clause.
66
- if (addition) {
67
- parts.push(concat([hardline, path.call(print, "body", 3)]));
48
+ if (path.getValue().body[2]) {
49
+ parts.push(concat([hardline, path.call(print, "body", 2)]));
50
+ }
51
+
52
+ return group(concat(parts));
53
+ }
54
+
55
+ // This is a container node that we're adding into the AST that isn't present in
56
+ // Ripper solely so that we have a nice place to attach inline comments.
57
+ function printRescueEx(path, opts, print) {
58
+ const [exception, variable] = path.getValue().body;
59
+ const parts = [];
60
+
61
+ if (exception) {
62
+ let exceptionDoc = path.call(print, "body", 0);
63
+
64
+ if (Array.isArray(exceptionDoc)) {
65
+ const joiner = concat([",", line]);
66
+ exceptionDoc = group(join(joiner, exceptionDoc));
67
+ }
68
+
69
+ parts.push(" ", exceptionDoc);
70
+ }
71
+
72
+ if (variable) {
73
+ parts.push(" => ", path.call(print, "body", 1));
68
74
  }
69
75
 
70
76
  return group(concat(parts));
@@ -89,6 +95,7 @@ module.exports = {
89
95
  ensure: printEnsure,
90
96
  redo: literal("redo"),
91
97
  rescue: printRescue,
98
+ rescue_ex: printRescueEx,
92
99
  rescue_mod: printRescueMod,
93
100
  retry: literal("retry")
94
101
  };
@@ -12,15 +12,13 @@ const {
12
12
  trim
13
13
  } = require("../../prettier");
14
14
 
15
+ const { isEmptyStmts } = require("../../utils");
16
+
15
17
  function printBodyStmt(path, opts, print) {
16
18
  const [stmts, rescue, elseClause, ensure] = path.getValue().body;
17
19
  const parts = [];
18
20
 
19
- if (
20
- stmts.body.length > 1 ||
21
- stmts.body[0].type != "void_stmt" ||
22
- stmts.body[0].comments
23
- ) {
21
+ if (!isEmptyStmts(stmts)) {
24
22
  parts.push(path.call(print, "body", 0));
25
23
  }
26
24
 
@@ -79,6 +77,9 @@ module.exports = {
79
77
  const { body } = path.getValue();
80
78
  return concat([trim, "__END__", literalline, body]);
81
79
  },
80
+ "@comment"(path, opts, _print) {
81
+ return opts.printer.printComment(path);
82
+ },
82
83
  bodystmt: printBodyStmt,
83
84
  paren: printParen,
84
85
  program: (path, opts, print) =>
@@ -4,6 +4,7 @@ const {
4
4
  hardline,
5
5
  indent,
6
6
  literalline,
7
+ removeLines,
7
8
  softline,
8
9
  join
9
10
  } = require("../../prettier");
@@ -103,14 +104,14 @@ function printStringDVar(path, opts, print) {
103
104
  }
104
105
 
105
106
  function printStringEmbExpr(path, opts, print) {
107
+ const node = path.getValue();
106
108
  const parts = path.call(print, "body", 0);
107
109
 
108
- // If the interpolated expression is inside of a heredoc or an xstring
109
- // literal (a string that gets sent to the command line) then we don't want
110
- // to automatically indent, as this can lead to some very odd looking
111
- // expressions
112
- if (["heredoc", "xstring_literal"].includes(path.getParentNode().type)) {
113
- return concat(["#{", parts, "}"]);
110
+ // If the contents of this embedded expression were originally on the same
111
+ // line in the source, then we're going to leave them in place and assume
112
+ // that's the way the developer wanted this expression represented.
113
+ if (node.sl === node.el) {
114
+ return concat(["#{", removeLines(parts), "}"]);
114
115
  }
115
116
 
116
117
  return group(
data/src/ruby/parser.js CHANGED
@@ -1,59 +1,11 @@
1
- const { spawnSync } = require("child_process");
2
- const path = require("path");
3
-
4
- // In order to properly parse ruby code, we need to tell the ruby process to
5
- // parse using UTF-8. Unfortunately, the way that you accomplish this looks
6
- // differently depending on your platform.
7
- /* istanbul ignore next */
8
- const LANG = (() => {
9
- const { env, platform } = process;
10
- const envValue = env.LC_ALL || env.LC_CTYPE || env.LANG;
11
-
12
- // If an env var is set for the locale that already includes UTF-8 in the
13
- // name, then assume we can go with that.
14
- if (envValue && envValue.includes("UTF-8")) {
15
- return envValue;
16
- }
17
-
18
- // Otherwise, we're going to guess which encoding to use based on the system.
19
- // This is probably not the best approach in the world, as you could be on
20
- // linux and not have C.UTF-8, but in that case you're probably passing an env
21
- // var for it. This object below represents all of the possible values of
22
- // process.platform per:
23
- // https://nodejs.org/api/process.html#process_process_platform
24
- return {
25
- aix: "C.UTF-8",
26
- darwin: "en_US.UTF-8",
27
- freebsd: "C.UTF-8",
28
- linux: "C.UTF-8",
29
- openbsd: "C.UTF-8",
30
- sunos: "C.UTF-8",
31
- win32: ".UTF-8"
32
- }[platform];
33
- })();
1
+ const parseSync = require("../parser/parseSync");
34
2
 
35
3
  // This function is responsible for taking an input string of text and returning
36
4
  // to prettier a JavaScript object that is the equivalent AST that represents
37
5
  // the code stored in that string. We accomplish this by spawning a new Ruby
38
6
  // process of parser.rb and reading JSON off STDOUT.
39
7
  function parse(text, _parsers, _opts) {
40
- const child = spawnSync(
41
- "ruby",
42
- ["--disable-gems", path.join(__dirname, "./parser.rb")],
43
- {
44
- env: Object.assign({}, process.env, { LANG }),
45
- input: text,
46
- maxBuffer: 15 * 1024 * 1024 // 15MB
47
- }
48
- );
49
-
50
- const error = child.stderr.toString();
51
- if (error) {
52
- throw new Error(error);
53
- }
54
-
55
- const response = child.stdout.toString();
56
- return JSON.parse(response);
8
+ return parseSync("ruby", text);
57
9
  }
58
10
 
59
11
  const pragmaPattern = /#\s*@(prettier|format)/;
data/src/ruby/parser.rb CHANGED
@@ -17,10 +17,48 @@ require 'delegate'
17
17
  require 'json'
18
18
  require 'ripper'
19
19
 
20
- module Prettier; end
20
+ module Prettier
21
+ end
21
22
 
22
23
  class Prettier::Parser < Ripper
23
- attr_reader :source, :lines, :scanner_events, :line_counts
24
+ # Represents a line in the source. If this class is being used, it means that
25
+ # every character in the string is 1 byte in length, so we can just return the
26
+ # start of the line + the index.
27
+ class SingleByteString
28
+ def initialize(start)
29
+ @start = start
30
+ end
31
+
32
+ def [](byteindex)
33
+ @start + byteindex
34
+ end
35
+ end
36
+
37
+ # Represents a line in the source. If this class is being used, it means that
38
+ # there are characters in the string that are multi-byte, so we will build up
39
+ # an array of indices, such that array[byteindex] will be equal to the index
40
+ # of the character within the string.
41
+ class MultiByteString
42
+ def initialize(start, line)
43
+ @indices = []
44
+
45
+ line
46
+ .each_char
47
+ .with_index(start) do |char, index|
48
+ char.bytesize.times { @indices << index }
49
+ end
50
+ end
51
+
52
+ def [](byteindex)
53
+ @indices[byteindex]
54
+ end
55
+ end
56
+
57
+ attr_reader :source, :lines, :scanner_events
58
+
59
+ # This is an attr_accessor so Stmts objects can grab comments out of this
60
+ # array and attach them to themselves.
61
+ attr_accessor :comments
24
62
 
25
63
  def initialize(source, *args)
26
64
  super(source, *args)
@@ -35,9 +73,23 @@ class Prettier::Parser < Ripper
35
73
  @heredocs = []
36
74
 
37
75
  @scanner_events = []
38
- @line_counts = [0]
76
+ @line_counts = []
77
+
78
+ # Here we're going to build up a list of SingleByteString or MultiByteString
79
+ # objects. They're each going to represent a string in the source. They are
80
+ # used by the `char_pos` method to determine where we are in the source
81
+ # string.
82
+ last_index = 0
83
+
84
+ @source.lines.each do |line|
85
+ if line.size == line.bytesize
86
+ @line_counts << SingleByteString.new(last_index)
87
+ else
88
+ @line_counts << MultiByteString.new(last_index, line)
89
+ end
39
90
 
40
- @source.lines.each { |line| @line_counts << @line_counts.last + line.size }
91
+ last_index += line.size
92
+ end
41
93
  end
42
94
 
43
95
  def self.parse(source)
@@ -55,7 +107,7 @@ class Prettier::Parser < Ripper
55
107
  # this line, then we add the number of columns into this line that we've gone
56
108
  # through.
57
109
  def char_pos
58
- line_counts[lineno - 1] + column
110
+ @line_counts[lineno - 1][column]
59
111
  end
60
112
 
61
113
  # As we build up a list of scanner events, we'll periodically need to go
@@ -125,6 +177,7 @@ class Prettier::Parser < Ripper
125
177
  @comments << {
126
178
  type: :@comment,
127
179
  value: value[1..-1].chomp.force_encoding('UTF-8'),
180
+ inline: value.strip != lines[lineno - 1],
128
181
  sl: lineno,
129
182
  el: lineno,
130
183
  sc: char_pos,
@@ -512,6 +565,12 @@ class Prettier::Parser < Ripper
512
565
  # binary is a parser event that represents a binary operation between two
513
566
  # values.
514
567
  def on_binary(left, oper, right)
568
+ # On most Ruby implementations, oper is a Symbol that represents that
569
+ # operation being performed. For instance in the example `1 < 2`, the `oper`
570
+ # object would be `:<`. However, on JRuby, it's an `@op` node, so here we're
571
+ # going to explicitly convert it into the same normalized form.
572
+ oper = scanner_events.delete(oper)[:body] unless oper.is_a?(Symbol)
573
+
515
574
  {
516
575
  type: :binary,
517
576
  body: [left, oper, right],
@@ -1670,6 +1729,28 @@ class Prettier::Parser < Ripper
1670
1729
  )
1671
1730
  end
1672
1731
 
1732
+ # A special parser error so that we can get nice syntax displays on the error
1733
+ # message when prettier prints out the results.
1734
+ class ParserError < StandardError
1735
+ attr_reader :lineno, :column
1736
+
1737
+ def initialize(error, lineno, column)
1738
+ super(error)
1739
+ @lineno = lineno
1740
+ @column = column
1741
+ end
1742
+ end
1743
+
1744
+ # If we encounter a parse error, just immediately bail out so that our runner
1745
+ # can catch it.
1746
+ def on_parse_error(error, *)
1747
+ raise ParserError.new(error, lineno, column)
1748
+ end
1749
+ alias on_alias_error on_parse_error
1750
+ alias on_assign_error on_parse_error
1751
+ alias on_class_name_error on_parse_error
1752
+ alias on_param_error on_parse_error
1753
+
1673
1754
  # The program node is the very top of the AST. Here we'll attach all of
1674
1755
  # the comments that we've gathered up over the course of parsing the
1675
1756
  # source string. We'll also attach on the __END__ content if there was
@@ -1768,8 +1849,8 @@ class Prettier::Parser < Ripper
1768
1849
  def bind_end(ec)
1769
1850
  merge!(ec: ec)
1770
1851
 
1771
- stmts = self[:body][2]
1772
- consequent = self[:body][3]
1852
+ stmts = self[:body][1]
1853
+ consequent = self[:body][2]
1773
1854
 
1774
1855
  if consequent
1775
1856
  consequent.bind_end(ec)
@@ -1784,16 +1865,30 @@ class Prettier::Parser < Ripper
1784
1865
  # inside of a bodystmt.
1785
1866
  def on_rescue(exceptions, variable, stmts, consequent)
1786
1867
  beging = find_scanner_event(:@kw, 'rescue')
1868
+ exceptions = exceptions[0] if exceptions.is_a?(Array)
1787
1869
 
1788
- last_exception = exceptions.is_a?(Array) ? exceptions[-1] : exceptions
1789
- last_node = variable || last_exception || beging
1790
-
1870
+ last_node = variable || exceptions || beging
1791
1871
  stmts.bind(find_next_statement_start(last_node[:ec]), char_pos)
1792
1872
 
1873
+ # We add an additional inner node here that ripper doesn't provide so that
1874
+ # we have a nice place to attach inline comment. But we only need it if we
1875
+ # have an exception or a variable that we're rescuing.
1876
+ rescue_ex =
1877
+ if exceptions || variable
1878
+ {
1879
+ type: :rescue_ex,
1880
+ body: [exceptions, variable],
1881
+ sl: beging[:sl],
1882
+ sc: beging[:ec] + 1,
1883
+ el: last_node[:el],
1884
+ ec: last_node[:ec]
1885
+ }
1886
+ end
1887
+
1793
1888
  Rescue.new(
1794
1889
  beging.merge!(
1795
1890
  type: :rescue,
1796
- body: [exceptions, variable, stmts, consequent],
1891
+ body: [rescue_ex, stmts, consequent],
1797
1892
  el: lineno,
1798
1893
  ec: char_pos
1799
1894
  )
@@ -1892,12 +1987,21 @@ class Prettier::Parser < Ripper
1892
1987
  # propagate that onto void_stmt nodes inside the stmts in order to make sure
1893
1988
  # all comments get printed appropriately.
1894
1989
  class Stmts < SimpleDelegator
1990
+ attr_reader :parser
1991
+
1992
+ def initialize(parser, values)
1993
+ @parser = parser
1994
+ __setobj__(values)
1995
+ end
1996
+
1895
1997
  def bind(sc, ec)
1896
1998
  merge!(sc: sc, ec: ec)
1897
1999
 
1898
2000
  if self[:body][0][:type] == :void_stmt
1899
2001
  self[:body][0].merge!(sc: sc, ec: sc)
1900
2002
  end
2003
+
2004
+ attach_comments(sc, ec)
1901
2005
  end
1902
2006
 
1903
2007
  def bind_end(ec)
@@ -1914,6 +2018,22 @@ class Prettier::Parser < Ripper
1914
2018
  self[:body] << statement
1915
2019
  self
1916
2020
  end
2021
+
2022
+ private
2023
+
2024
+ def attach_comments(sc, ec)
2025
+ attachable =
2026
+ parser.comments.select do |comment|
2027
+ comment[:type] == :@comment && !comment[:inline] &&
2028
+ sc <= comment[:sc] && ec >= comment[:ec] &&
2029
+ !comment[:value].include?('prettier-ignore')
2030
+ end
2031
+
2032
+ return if attachable.empty?
2033
+
2034
+ parser.comments -= attachable
2035
+ self[:body] = (self[:body] + attachable).sort_by! { |node| node[:sc] }
2036
+ end
1917
2037
  end
1918
2038
 
1919
2039
  # stmts_new is a parser event that represents the beginning of a list of
@@ -1921,6 +2041,7 @@ class Prettier::Parser < Ripper
1921
2041
  # stmts_add events, which we'll append onto an array body.
1922
2042
  def on_stmts_new
1923
2043
  Stmts.new(
2044
+ self,
1924
2045
  type: :stmts,
1925
2046
  body: [],
1926
2047
  sl: lineno,
@@ -2549,23 +2670,3 @@ class Prettier::Parser < Ripper
2549
2670
  find_scanner_event(:@kw, 'super').merge!(type: :zsuper)
2550
2671
  end
2551
2672
  end
2552
-
2553
- # If this is the main file we're executing, then most likely this is being
2554
- # executed from the parser.js spawn. In that case, read the ruby source from
2555
- # stdin and report back the AST over stdout.
2556
-
2557
- if $0 == __FILE__
2558
- response = Prettier::Parser.parse($stdin.read)
2559
-
2560
- if !response
2561
- warn(
2562
- '@prettier/plugin-ruby encountered an error when attempting to parse ' \
2563
- 'the ruby source. This usually means there was a syntax error in the ' \
2564
- 'file in question. You can verify by running `ruby -i [path/to/file]`.'
2565
- )
2566
-
2567
- exit 1
2568
- end
2569
-
2570
- puts JSON.fast_generate(response)
2571
- end