Tips - String literal in block comments
どこかで役に立てば、という程度の無駄知識。
ポイントは、
/*eLsE ccc + 44 '*/' */で、コメントが変な風に中断したり
空白のスキップとクォート出来る様にしてみた。 - 設計と実装の狭間で。
というところで、逆にコメントを変な風に中断する方法について。
実はコメントを変な風に中断するほうが難しい例です。
Javaでの文字列リテラル
STRING : '"' STRING_ELEMENT* '"' ; fragment STRING_ELEMENT : ~( '"' | '\\' | '\r' | '\n' ) | ESCAPE_SEQUENCE ; fragment ESCAPE_SEQUENCE : '\\' ( ('b'|'t'|'n'|'f'|'r'|'\"'|'\''|'\\') | 'u'+ HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT | OCT_DIGIT | OCT_DIGIT OCT_DIGIT | ( '0'..'3' ) OCT_DIGIT OCT_DIGIT ) ; fragment OCT_DIGIT : '0'..'7' ; fragment HEX_DIGIT : '0'..'9' | 'a'..'f' | 'A'..'F' ;
STRING_ELEMENTの解釈は下のような感じで、""内に出現する1文字分と解釈します。
fragment STRING_ELEMENT : ~( '"' | '\\' | '\r' | '\n' ) | ESCAPE_SEQUENCE ; fragment ESCAPE_SEQUENCE : '\\' ( ('b'|'t'|'n' ...
つまり、
- ", \, [CR], [LF] のいずれでもない : ~( '"' | '\\' | '\r' | '\n' )
- \から始まるエスケープシーケンスである : '\\' ...
のいずれかです。
ただしこれだと、コメント内に"*/"という文字列があった場合、
/* "*/"".toString(); /* */
セマンティクスが盛大に変わってしまいます。
「*/」を含まない文字列
ブロックコメント内で 「*/」 が出現した場合、そこでブロックコメントが終了します。
まず、sematic predictionを利用して、ブロックコメントにいる場合とそうでない場合のルールを分岐します。
STRING : { !inComment || inLineComment }? '"' STRING_ELEMENT* '"' | { inComment && !inLineComment }? '"' STRING_BODY_IN_COMMENT '"' ;
これで、STRING_BODY_IN_COMMENTがブロックコメント内での""内に出現する内容になります。
2文字以上からなる例外をANTLRの構文で書くのは実は非常にめんどうで、下のようには書けません。
fragment STRING_BODY_IN_COMMENT : STRING_ELEMENT_IN_COMMENT* ; fragment STRING_ELEMENT_IN_COMMENT : ~( '"' | '\\' | '\r' | '\n' | '*/' ) // BAD RULE | ESCAPE_SEQUENCE ;
1文字を除くだけなら ~X の形式で書けばよいのですが、2文字以上の場合はうまくいきません。
ということで、ちゃんとオートマトンを意識して書かないとだめぽ。
オートマトン
文字の遷移状態を元に、ブロックコメント内に出現可能なサブ文字列を考えます。
- 1文字目が"*"でないサブ文字列
- ", \, [CR], [LF], * のいずれでもない : ~( '"' | '\\' | '\r' | '\n' | '*' )
- \から始まるエスケープシーケンスである : '\\' ...
- 1文字目が"*"であるサブ文字列
- "*" から始まり、末尾が ", \, [CR], [LF], /, * のいずれでもない : '*'+ ~( '"' | '\\' | '\r' | '\n' | '/' | '*' )
- "*" から始まり、末尾が エスケープシーケンスである : '*'+ '\\' ...
- "*" から始まり、後続する文字列が存在しない : '*'+ (終端)
上記を網羅すれば、「*/」というパターンを排除した文字列リテラルを指定できます。
fragment STRING_BODY_IN_COMMENT : STRING_ELEMENT_IN_COMMENT* ( '*'+ )? // 2-3 ; fragment STRING_ELEMENT_IN_COMMENT : ~( '"' | '\\' | '\r' | '\n' | '*' ) // 1-1 | ESCAPE_SEQUENCE // 1-2 | '*'+ ~( '"' | '\\' | '\r' | '\n' | '*' | '/' ) // 2-1 | '*'+ ESCAPE_SEQUENCE // 2-2 ;
2-3がちょっとトリッキーな感じです。STRING_BODY_IN_COMMENTの最後に( '*'+ )?と書くことによって、末尾に1文字以上の"*"があってもよい、ということで「"*" から始まり、後続する文字列が存在しない」というルールの可能性を表現します。
読みやすいようにリファクタリングすると、最終的にこうなります。
STRING : { !inComment || inLineComment }? '"' STRING_ELEMENT* '"' | { inComment && !inLineComment }? '"' STRING_ELEMENT_IN_COMMENT* '*'* '"' ; fragment STRING_ELEMENT : ~( '"' | '\\' | '\r' | '\n' ) | ESCAPE_SEQUENCE ; fragment STRING_ELEMENT_IN_COMMENT : ~( '"' | '\\' | '\r' | '\n' | '*' ) | ESCAPE_SEQUENCE | '*'+ ~( '"' | '\\' | '\r' | '\n' | '*' | '/' ) | '*'+ ESCAPE_SEQUENCE ;
こんなのでテストしました。
"OK" -- "OK" /* "OK" */ "OK: */" -- "OK: */" /* "OK: /*" */ /* "OK: *?" */ /* "NG!: */" */
最後のルールだけ、"NG!: */"というトークンでエラーになります。