Created
November 26, 2015 11:36
-
-
Save snipsnipsnip/9c14aa2f41375e086a15 to your computer and use it in GitHub Desktop.
tom.rb: primitive JSP lint
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# coding: utf-8 | |
# | |
# tom: JSPのタグ開閉の対応チェッカ | |
# MIT license, @snipsnipsnip | |
# | |
=begin | |
JSPの文法は普通のHTMLと違ってかなりゆるい。 | |
・特殊なタグがある(<%%>とか<s:text>とか)。 | |
後者はXMLのネームスペース記法のように見えるが、実際taglibという特殊タグがファイルの頭に書いてあればルートノードにxmlns属性がなくても動くのでたぶん別物。 | |
・タグ属性内にタグを書くことができ、その中ではダブルクォートを使って良い。<hoge fuga="<foo bar="baz"/>"> | |
・タグ属性内に展開式(OGNL)を書くことができ、その中ではダブルクォートを使って良い。 <hoge fuga="%{"hoge"}"> | |
・タグ属性内に式(OGNL)をそのまま書くことができ、その中では大なり小なりを使って良い。 <s:if test="foo > 3"> | |
・HTMLタグ内にタグを書くこともできる。 <hoge <fuga/> > | |
・通常のコメントと別にJSPのコメントがある。 <%-- foo --%> | |
・タグの開きと閉じが対応する必要はない。 | |
<s:if>,<s:elseif>,<s:else>といったタグの中に開きタグがある場合、出力されるのはどれか一つなので、その出力後のHTMLがつりあいがとれていればブラウザで表示ができる。 | |
これは対応がめんどいので普通にエラー扱いにする。 | |
=end | |
require 'strscan' | |
class Tom | |
SELF_CLOSING_TAGS = /\A(?:meta|br)\z/i | |
def initialize(str) | |
@s = StringScanner.new(str) | |
@stack = [] | |
@line = 1 | |
end | |
def check | |
s = @s | |
stack = @stack | |
until eos? | |
skip(/\s*/) | |
case state | |
when :double_quote | |
skip /[^"<]*/ | |
case | |
when skip(/"/) | |
stack.pop | |
when skip(/<[a-z:]+/i) | |
stack.push s[0][1..-1], '<' | |
when skip(/</) | |
when !eos? | |
unknown_pattern | |
end | |
when :single_quote | |
skip /[^'<]*/ | |
case | |
when skip(/'/) | |
stack.pop | |
when skip(/<[a-z:]+/i) | |
stack.push s[0][1..-1], '<' | |
when skip(/</) | |
when !eos? | |
unknown_pattern | |
end | |
when :tag | |
skip(/[\-\s=a-z]*/i) | |
case | |
when skip(/<[a-z:]+/i) | |
stack.push s[0][1..-1], '<' | |
when skip(/\/>/) | |
stack.pop(2) | |
when skip(/>/) | |
if stack[-2] =~ SELF_CLOSING_TAGS | |
stack.pop(2) | |
else | |
stack.pop | |
end | |
when skip(/"/) | |
stack.push '"' | |
when skip(/'/) | |
stack.push "'" | |
when !eos? | |
unknown_pattern | |
end | |
when :comment | |
skip(/[^\-]*/) | |
if skip(/-->/) | |
stack.pop | |
elsif skip(/-/) | |
elsif !eos? | |
unknown_pattern | |
end | |
when :script | |
skip(/[^<]*/) | |
if skip(/<\/script>/) | |
stack.pop | |
elsif skip(/</) | |
elsif !eos? | |
unknown_pattern | |
end | |
when :style | |
skip(/[^<]*/) | |
if skip(/<\/style>/) | |
stack.pop | |
elsif skip(/</) | |
elsif !eos? | |
unknown_pattern | |
end | |
when :jsp_tag | |
skip(/[^%]*/) | |
if skip(/%>/) | |
stack.pop | |
elsif skip(/%/) | |
elsif !eos? | |
unknown_pattern | |
end | |
when :body | |
skip(/[^<>]*/) | |
case | |
when skip(/<!--/) | |
stack.push '<!--' | |
when skip(/<%/) | |
stack.push '<%' | |
when skip(/<[a-z:]+/i) | |
stack.push s[0][1..-1], '<' | |
when skip(/<\/([a-z:]+)\s*>/i) | |
if s[1] == stack.last | |
stack.pop | |
else | |
raise "#{s[1]} を閉じようとしましたが #{stack.last} が閉じていません (#{@line}行目)\n#{@s.peek(30).inspect}.." | |
end | |
when !eos? | |
unknown_pattern | |
end | |
when :root | |
skip(/[^<>]*/) | |
if skip(/<%/) | |
stack.push '<%' | |
elsif skip(/<[^\s>]+/) | |
stack.push s[0][1..-1], '<' | |
elsif !eos? | |
unknown_pattern | |
end | |
else | |
raise "unexpected state: #{@stack.last} (#{@line}行目)" | |
end | |
end | |
unless stack.empty? | |
raise "閉じ括弧が足りません: #{stack.inspect}" | |
end | |
end | |
private | |
def unknown_pattern | |
raise "unknown pattern: #{@s.peek(30).inspect}.. (#{@line}行目)" | |
end | |
def eos? | |
@s.eos? | |
end | |
def skip(pat) | |
if r = @s.skip(pat) | |
@line += @s[0].count("\n") | |
end | |
r | |
end | |
def state | |
case @stack.last | |
when /\A'\z/ | |
:single_quote | |
when /\A"\z/ | |
:double_quote | |
when /\A<\z/ | |
:tag | |
when /\A<%\z/ | |
:jsp_tag | |
when /\A<!--\z/ | |
:comment | |
when /\Astyle\z/i | |
:style | |
when /\Ascript\z/i | |
:script | |
when /\A[a-z:]+\z/i | |
:body | |
when nil | |
:root | |
else | |
:wtf | |
end | |
end | |
def self.main | |
ARGV.flat_map {|a| Dir[a] }.each do |a| | |
puts File.basename(a) | |
Tom.new(File.read(a, encoding: 'utf-8')).check | |
end | |
end | |
end | |
Tom.main if __FILE__ == $0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment