summaryrefslogtreecommitdiffstats
path: root/indenters/ruby_formatter.rb
blob: acd315049221f81fac562257d62bca428e90370b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
#!/usr/bin/ruby -w
#
# == Synopsis
#
# Simple Ruby Formatter
#
# Created by: Stephen Becker IV
# Contributions: Andrew Nutter-Upham
# Contact: sbeckeriv@gmail.com
# SVN: http://svn.stephenbeckeriv.com/code/ruby_formatter/
#
# Its been done before RadRails did,
# http://vim.sourceforge.net/tips/tip.php?tip_id = 1368 that guy did it, but I did
# not look for a ruby formatter until i was done.
#
# It is called simple formatting because it is. I have the concept of 3 differnt
# indent actions In, Out and Both. I have mixed the concept of indenting and
# outdenting. Out means you have added white space and in means you remove a layer
# of white space.
#
# Basic logic
# 	Decrease current depth if
# 			((the it is not a one line if unless statment
# 			(need to lookfor more oneline blocks) and it ends with end
# 			or if the } count is larger then {)
# 		or
# 			the first word is in the both list)
#
# 		and
# 			depth is larger then zero
#
# 	Increase current depth if
# 			It is not a one liner
# 		and
# 			(the word is in the out list
# 		or
# 			the word is in the both list
# 		or
# 			it looks like a start block)
# 		and
# 			temp_depth is nil (used for = comment blocks)
#
#
# Sure there are some regx's and a crap load of gsubs, but it still simple. Its
# not like its a pychecker (http://www.metaslash.com/brochure/ipc10.html)
#
# == Usage
#
# ruby [options] filelist
#
# options:
#   -s # will change the indent to a space count of # per level
#        by default we space with 1 tab per level
#   -b # create a backup file
#
# examples:
# ruby simple_formatter.rb -s 3 -b /moo/cluck/cow.rb
# runs with the indent of 3 spaces,creates a backup file, and formats moo/cluck/cow.rb
#
#
# Tested with random files off of koders.com
#
#
::DEBUG_ME = false
require 'getoptlong'
require "fileutils"
require "pp"
$escape_strings = {:regex=>"EsCaPedReGex",:string=>"EsCaPeDStRiNg"}
begin
	require 'rdoc/usage'
rescue Exception => e
	#eat the no load of rdocs?
end
opts = GetoptLong.new(
[ '--help', '-h', GetoptLong::NO_ARGUMENT ],
[ '--spaces', '-s', GetoptLong::OPTIONAL_ARGUMENT ],
[ '--debug', '-d', GetoptLong::NO_ARGUMENT ],
[ '--backup', '-b', GetoptLong::NO_ARGUMENT ]
)
space_count = nil
backup = false
files = []
opts.each do | opt, arg|
	case opt
	when '--help'
		begin
			RDoc::usage
		rescue Exception =>e
			puts "If you want to use rdocs you need to install it"
			exit(-1)
		end
	when '--spaces'
		space_count = arg.to_i
	when '--backup'
		backup = true
	when '--debug'
		::DEBUG_ME = true
	end
end
require "profile" if ::DEBUG_ME
if ARGV.length < 1
	puts "Missing filelist argument (try --help)"
	exit 0
end
array_loc = ARGV
#find if the string is a start block
#return true if it is
#rules
# does not include end at the end
# and ( { out number the } or it includes do
DO_RX = /\sdo\s*$/
def start_block?(string)
	return true if  string.gsub(/\|.*\|/, "").match(DO_RX) || (string.scan(/\{/).size > string.scan(/\}/).size)
	false
end
#is this an end block?
#rules
#its not a one liner
#and it ends with end
#or } out number {
CHECK_ENDS_RX = /end$|end\s+while/
def check_ends?(string)
	#check for one liners end and }
	#string = just_the_string_please(string)
	return true if (string.scan(/\{/).size < string.scan(/\}/).size) || string.match(CHECK_ENDS_RX)
	false
end
IN_OUTS_RX = /^(def|class|module|begin|case|if|unless|loop|while|until|for)/
#look at first work does it start with one of the out works
def in_outs?(string)
	string.sub!(/\(.*\)/, "")
	return true if string.lstrip.match(IN_OUTS_RX) && string.strip.size == $1.strip.size
	false
end
IN_BOTH_RX = /^(elsif|else|when|rescue|ensure)/
#look at first work does it start with one of the both words?
def in_both?(string)
	return true if string.lstrip.match(IN_BOTH_RX) && string.strip.size == $1.strip.size
	false
end
#extra formatting for the line
#we wrap = with spaces
#JUST_STRING_PLEASE_RX = /^#.*|\/.*\/|"([^"])*"|'([^'])*'/
LINE_CLEAN_UP_RX = /[a-zA-Z\]\'\"{\d]+=[a-zA-Z\[\'\"{\d]+/
def line_clean_up(x)
	#this formatts strings and regexs remove and add in replacement works

	x.gsub!(/\\\//,$escape_strings[:regex])
	strings = x.scan(/#.*|["'\/].*?["'\/]/)
	strings.each { | str |
		x.sub!(str, $escape_strings[:string])
	}
	#lofted code from java formatter #{add in link}
	# replace "){" with ") {"
	x.sub!(/\)\s*\{/, ") {")
	# replace "return(" with "return ("
	# replace "if(" with "if ("
	# replace "while(" with "while ("
	# replace "switch(" with "switch ("    ruby does not have a switch 
	# replace "catch(" with "catch ("
	x.sub!(/\b(return|if|elsif|while|case|catch)\s*\(/, '\1 (')
	# replace " ;" with ";"
	# replace " ," with ","
	x.gsub!(/\s+([\;\,])/, '\1')
	#replace ",abc" with ", abc"
	x.gsub!(/\,(\w+)/, ', \1')

	x.gsub!(/(\)|"|\w)\s*([\+\-\*\/\&\|\^\%]|\&\&|\|\||[\>\<]|\>\=|\<\=|\=\=|\!\=|\<\<|\>\>|\>\>\>)\s*(?=(\w | "))/, '\1 \2 ')
	# a space before and after AssignmentOperator
	x.gsub!(/(\w)\s*(\+\=|\-\=|\*\=|\/\=|\&\=|\|\=|\^\=|\%\=|\<\<\=|\>\>\=|\>\>\>\=)\s*(?=(\w))/, '\1 \2 ')
	# do not trim spaces
	x.gsub!(/(\w)\=\s*(?=(\w|"))/, '\1 = ')
	x.gsub!(/(\w)\s*\=(?=(\w|"))/, '\1 = ')
	#becker format
	#not complete list but alot of the common ones.
	x.sub!(/(\.each|\.collect[!]*|\.map[!]*|\.delete_if|\.sort[!]*|\.each_[pair|key|value|byte|with_index|line|option]|\.reject[!]*|\.reverse_each|\.detect|\.find[_all]*|\.select|\.module_eval|\.all_waits|loop|proc|lambda|fork|at_exit)\s*\{/, '\1 {')
	x.sub!(/def\s(\w*)?(\(.*?\))/, 'def \1\2') if x.match(/def\s+?(\w*)?\(.*?\)/)
	x.sub!(/^for\s+(\w*)?\s+in\s+?(.*)$/, 'for \1 in \2') if x.match(/^for\s+(\w*)?\s*?in\s*?(.*)$/)
	x.gsub!(/(\w)\=>\s*(?=(\w|"|:))/, '\1 => ')
	x.gsub!(/(\w)\s*\=>(?=(\w|"|:))/, '\1 => ')
	x.strip!
	x.gsub!($escape_strings[:string]) {
		strings.shift
	}
	x.gsub!($escape_strings[:regex], "\\\/")
	return x
end

JUST_STRING_PLEASE_RX = /\/.*\/|"([^"])*" | '([^']) * '|#.*/
def just_the_string_please(org_string)
	string = String.new(org_string)
	#remove escaped chars
	string.gsub!(/\\\/|\\"|\\'/, "")
	string.gsub!(JUST_STRING_PLEASE_RX, "")
	string = string.strip
	string.sub!(/\b(return|if|while|case|catch)\s*\(/, '\1 (')
	puts "clean string: #{string}" if ::DEBUG_ME
	string
end
ONE_LINER_RX = /(unless|if).*(then).*end|(begin).*(rescue|ensure|else).*end/
def one_liner?(string)
	return true if string.match(ONE_LINER_RX)
	false
end

array_loc.each {|file_loc|
	f = File.open(file_loc, "r")
	text = f.read
	f.close
	if File.expand_path(file_loc) == File.expand_path($0)
		$escape_strings = {:regex=>"EsCaPedReGex#{rand(200)}",:string=>"EsCaPeDStRiNg#{rand(200)}"}
	end
	new_text = ""
	current_depth = 0
	spaces =  " " * space_count if space_count
	here_doc_ending = nil
	indenter = spaces || "\t"
	temp_depth = nil
	line_count = 1

	text.split("\n").each { |x|
		#comments
		#The first idea was to leave them alone.
		#after running a few test i did not like the way it looked
		if temp_depth
			puts "In temp_depth #{x} line ♯ #{line_count} here:#{here_doc_ending}" if ::DEBUG_ME
			new_text << x << "\n"
			#block comments, its going to get ugly
			if !x.lstrip.scan(/^\=end/).empty? || (here_doc_ending && x.strip == here_doc_ending.strip)
				#swap and set
				puts "swap and set #{x} line # #{line_count}" if ::DEBUG_ME
				current_depth = temp_depth
				temp_depth = nil
				here_doc_ending = nil
			end
			line_count += 1
			next
		end
		#block will always be 0 depth
		#block comments, its going to get ugly
		unless x.lstrip.scan(/^\=begin/).empty?
			#swap and set
			puts "Looking for begin #{x} #{line_count}" if ::DEBUG_ME
			temp_depth = current_depth
			current_depth = 0
		end
		#here docs have same type of logic for block comments
		unless x.lstrip.scan(/<<-/).empty?
			#swap and set
			here_doc_ending = x.lstrip.split(/<<-/).last.strip
			temp_depth = current_depth
		end
		#whats the first word?
		text_node = x.split.first || ""
		just_string = just_the_string_please(x)
		in_both = in_both?(text_node)
		one_liner = one_liner?(just_string)
		#check if its in end or both and that the current_depth is >0
		#maybe i should raise if it goes negative ?
		puts "minus one #{line_count} #{x} statement:#{(check_ends?(just_string) || in_both) && current_depth > 0} check_ends:#{check_ends?(just_string)} in_both:#{in_both}  current_depth:#{ current_depth }" if ::DEBUG_ME
		if (check_ends?(just_string) || in_both) &&  !one_liner
			puts "We have a Negative depth count. This was caused around line:#{line_count}\nCheck for if( it should be if (" if  current_depth == 0
			current_depth -= 1 unless current_depth == 0
		end
		clean_string = line_clean_up(x)
		current_indent = clean_string.size>0 ? indenter*current_depth : ""
		new_text << current_indent << clean_string << "\n"
		#we want to kick the indent out one
		#  x.match(/(unless|if).*(then).*end/): we use this match one liners for if statements not one-line blocks
		# in_outs? returns true if the first work is in the out array
		# in_both? does the same for the both array
		# start_block looks for to not have an end at the end and {.count > }.count and if the word do is in there
		# temp_depth is used when we hit the = comments should be nil unless you are in a comment
		puts "plus one match:#{line_count} #{x} not a one liner:#{!(one_liner)} or statements:#{(in_outs?(text_node) || in_both?(text_node) || start_block?(x))} in_outs#{in_outs?(text_node)} in_both:#{ in_both?(text_node)} start_block:#{ start_block?(x)} temp_depth:#{temp_depth}" if ::DEBUG_ME
		current_depth += 1 if ((in_outs?(text_node) || start_block?(just_string) || in_both || x.lstrip.slice(/\w*\s=\s(unless|if|case)/)) && !one_liner && !temp_depth)
		line_count += 1
	}
	FileUtils.cp("#{file_loc}","#{file_loc}.bk.#{Time.now.to_s.gsub(/\s|:/,"_")}") if backup
	f = File.open("#{file_loc}","w+")
	f.puts new_text
	f.close
	puts "Done!"
}