Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
29445cf
Ignore test.rb
kddnewton Nov 29, 2021
a0ca55f
Support for embedding comments in the tree
kddnewton Nov 29, 2021
4a9cecc
Formatting checkpoint
kddnewton Nov 19, 2021
1f6b2be
Tests for formatting
kddnewton Nov 19, 2021
66c05d8
prettyprint
kddnewton Nov 29, 2021
d70f5f2
Print comments surrounding the node
kddnewton Nov 29, 2021
f50f236
Get comments printing
kddnewton Nov 29, 2021
4a7d506
Update with test cases for comments
kddnewton Nov 29, 2021
25eefba
VarAlias tests
kddnewton Nov 29, 2021
00717fa
A bunch of tests for formatting keywords without arguments
kddnewton Nov 29, 2021
fdd718a
Tests for xstring_literal and yield
kddnewton Nov 29, 2021
43aed5c
Tests for special array literals
kddnewton Nov 29, 2021
e791625
Until, UntilMod, While, WhileMod tests
kddnewton Nov 29, 2021
d2318b2
IfMod and UnlessMod formatting
kddnewton Nov 29, 2021
1fbe689
Tests for various leaf nodes
kddnewton Nov 29, 2021
547d16f
Tests for SClass and StringConcat
kddnewton Nov 29, 2021
14bb1e3
Tests for ConstPathField, ConstPathRef, KwRestParam, RestParam, TopCo…
kddnewton Nov 29, 2021
24b0ff9
Tests for Undef
kddnewton Nov 30, 2021
76fbec6
Tests for Unary
kddnewton Nov 30, 2021
c878170
Tests for VarRef
kddnewton Nov 30, 2021
40a3251
Tests for VarField
kddnewton Nov 30, 2021
e88a6e2
Tests for Not, SymbolLiteral, VCall
kddnewton Nov 30, 2021
4de0091
Tests for Int, Module, MRHS, OpAssign, Program, StringDVar, and some …
kddnewton Nov 30, 2021
b2ec4bd
Tests for ArrayLiteral, BraceBlock, DoBlock, Hash, and IfOp
kddnewton Nov 30, 2021
b3701eb
Tests for Case and When
kddnewton Nov 30, 2021
7446c58
More tests for When
kddnewton Nov 30, 2021
a87cd06
Tests for RAssign
kddnewton Nov 30, 2021
81a604e
Tests for RAssign
kddnewton Nov 30, 2021
98aeacf
Tests for Command, Const, ConstRef, Def
kddnewton Nov 30, 2021
b076fe4
Tests for Defined, Dot2, Dot3, and ExcessedComma
kddnewton Nov 30, 2021
c8bdb1c
Better heredocs, tests for Heredoc, Defs, and DefEndless
kddnewton Nov 30, 2021
e562cc7
Tests for Elsif, EmbDoc, Params
kddnewton Nov 30, 2021
ee1f4ed
Tests for MLHSParen
kddnewton Nov 30, 2021
0ff165f
Tests for MLHS
kddnewton Nov 30, 2021
8bddece
Tests for Lambda and MAssign
kddnewton Nov 30, 2021
374611b
Tests for In and When
kddnewton Nov 30, 2021
73c584f
Tests for Break, FCall, Kw, Label, MethodAddArg, MethodAddBlock, Next…
kddnewton Nov 30, 2021
517d576
Tests for AccessCtrl, CommandCall, Else, Ensure, Field, FndPtn, For, …
kddnewton Nov 30, 2021
b9c355b
Tests for RegexpLiteral
kddnewton Nov 30, 2021
a926d56
Visitors to remove lines
kddnewton Dec 1, 2021
fa6c741
StringLiteral and DynaSymbol
kddnewton Dec 1, 2021
c139cb4
Add a better CLI that will format files
kddnewton Dec 1, 2021
257c5a5
Add a write mode for the CLI
kddnewton Dec 1, 2021
9f6d1f8
Format with stree
kddnewton Dec 1, 2021
ed2167b
Attach comments to statements as inline nodes directly
kddnewton Dec 1, 2021
41365d6
Fix up comment formatting
kddnewton Dec 1, 2021
30e9394
Format yourself
kddnewton Dec 1, 2021
f4682a1
Format the test suite
kddnewton Dec 1, 2021
7ab01a1
A bunch of these nodes do not need to go on the token list
kddnewton Dec 1, 2021
46d949a
Speed up
kddnewton Dec 1, 2021
29af9b7
Remove need for visitor
kddnewton Dec 1, 2021
410e7e3
Format everything
kddnewton Dec 1, 2021
613396e
Group stuff with nest
kddnewton Dec 1, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
StringLiteral and DynaSymbol
  • Loading branch information
kddnewton committed Dec 1, 2021
commit fa6c74102963f5223654a5eb83b834ec05a6caad
126 changes: 115 additions & 11 deletions lib/syntax_tree.rb
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ def initialize(source, *)
# check if certain lines contain certain characters. For example, we'll use
# this to generate the content that goes after the __END__ keyword. Or we'll
# use this to check if a comment has other content on its line.
@lines = source.split("\n")
@lines = source.split(/\r?\n/)

# This is the full set of comments that have been found by the parser. It's
# a running list. At the end of every block of statements, they will go in
Expand Down Expand Up @@ -4650,6 +4650,48 @@ def on_dot3(left, right)
)
end

# Responsible for providing information about quotes to be used for strings
# and dynamic symbols.
module Quotes
# The matching pairs of quotes that can be used with % literals.
PAIRS = { '(' => ')', '[' => ']', '{' => '}', '<' => '>' }.freeze

# If there is some part of this string that matches an escape sequence or
# that contains the interpolation pattern ("#{"), then we are locked into
# whichever quote the user chose. (If they chose single quotes, then double
# quoting would activate the escape sequence, and if they chose double
# quotes, then single quotes would deactivate it.)
def self.locked?(node)
node.parts.any? do |part|
part.is_a?(TStringContent) &&
(part.value.include?('#{') || part.value.include?('\\'))
end
end

# Find the matching closing quote for the given opening quote.
def self.matching(quote)
PAIRS.fetch(quote) { quote }
end

# Escape and unescape single and double quotes as needed to be able to
# enclose +content+ with +enclosing+.
def self.normalize(content, enclosing)
return content if enclosing != '"' && enclosing != "'"

content.gsub(/\\([\s\S])|(['"])/) do
_match, escaped, quote = Regexp.last_match.to_a

if quote == enclosing
"\\#{quote}"
elsif quote
quote
else
"\\#{escaped}"
end
end
end
end

# DynaSymbol represents a symbol literal that uses quotes to dynamically
# define its value.
#
Expand Down Expand Up @@ -4685,15 +4727,18 @@ def child_nodes
end

def format(q)
q.group do
# If we're inside of an assoc node as the key, then it will handle
# printing the : on its own since it could change sides.
parent = q.parent
q.text(':') if !parent.is_a?(Assoc) || parent.key != self
opening_quote, closing_quote = quotes(q)

q.text(q.quote)
q.format_each(parts)
q.text(q.quote)
q.group(0, opening_quote, closing_quote) do
parts.each do |part|
if part.is_a?(TStringContent)
value = Quotes.normalize(part.value, closing_quote)
separator = -> { q.breakable(force: true, indent: false) }
q.seplist(value.split(/\r?\n/, -1), separator) { |text| q.text(text) }
else
q.format(part)
end
end
end
end

Expand All @@ -4713,6 +4758,43 @@ def to_json(*opts)
*opts
)
end

private

# Here we determine the quotes to use for a dynamic symbol. It's bound by a
# lot of rules because it could be in many different contexts with many
# different kinds of escaping.
def quotes(q)
# If we're inside of an assoc node as the key, then it will handle
# printing the : on its own since it could change sides.
parent = q.parent
hash_key = parent.is_a?(Assoc) && parent.key == self

if quote.start_with?('%s')
# Here we're going to check if there is a closing character, a new line,
# or a quote in the content of the dyna symbol. If there is, then
# quoting could get weird, so just bail out and stick to the original
# quotes in the source.
matching = Quotes.matching(quote[2])
pattern = /[\n#{Regexp.escape(matching)}'"]/

if parts.any? { |part| part.is_a?(TStringContent) && part.value.match?(pattern) }
[quote, matching]
elsif Quotes.locked?(self)
["#{':' unless hash_key}'", "'"]
else
["#{':' unless hash_key}#{q.quote}", q.quote]
end
elsif Quotes.locked?(self)
if quote.start_with?(':')
[hash_key ? quote[1..-1] : quote, quote[1..-1]]
else
[hash_key ? quote : ":#{quote}", quote]
end
else
[hash_key ? q.quote : ":#{q.quote}", q.quote]
end
end
end

# :call-seq:
Expand Down Expand Up @@ -10444,8 +10526,30 @@ def child_nodes
end

def format(q)
q.group(0, quote, quote) do
q.format_each(parts)
if parts.empty?
q.text("#{q.quote}#{q.quote}")
return
end

opening_quote, closing_quote =
if !Quotes.locked?(self)
[q.quote, q.quote]
elsif quote.start_with?('%')
[quote, Quotes.matching(quote[/%[qQ]?(.)/, 1])]
else
[quote, quote]
end

q.group(0, opening_quote, closing_quote) do
parts.each do |part|
if part.is_a?(TStringContent)
value = Quotes.normalize(part.value, closing_quote)
separator = -> { q.breakable(force: true, indent: false) }
q.seplist(value.split(/\r?\n/, -1), separator) { |text| q.text(text) }
else
q.format(part)
end
end
end
end

Expand Down
22 changes: 22 additions & 0 deletions test/fixtures/dyna_symbol.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
%
:'foo'
-
:"foo"
%
:"foo"
%
:'foo #{bar}'
%
:"foo #{bar}"
%
%s[foo #{bar}]
-
:'foo #{bar}'
%
{ %s[foo] => bar }
-
{ "foo": bar }
%
%s[
foo
]
44 changes: 44 additions & 0 deletions test/fixtures/string_literal.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
%
%(foo \\ bar)
%
%[foo \\ bar]
%
%{foo \\ bar}
%
%<foo \\ bar>
%
%|foo \\ bar|
%
%q(foo \\ bar)
%
%q[foo \\ bar]
%
%q{foo \\ bar}
%
%q<foo \\ bar>
%
%q|foo \\ bar|
%
%Q(foo \\ bar)
%
%Q[foo \\ bar]
%
%Q{foo \\ bar}
%
%Q<foo \\ bar>
%
%Q|foo \\ bar|
%
''
-
""
%
'foo'
-
"foo"
%
'foo #{bar}'
%
'"foo"'
-
"\"foo\""