mirror of
https://gitea.invidious.io/iv-org/shard-ameba.git
synced 2024-08-15 00:53:29 +00:00
Large numbers rule + tokenizer (#10)
This commit is contained in:
parent
3f2bcc56b9
commit
2f9ba27811
7 changed files with 323 additions and 18 deletions
118
spec/ameba/rules/large_numbers_spec.cr
Normal file
118
spec/ameba/rules/large_numbers_spec.cr
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
require "../../spec_helper"
|
||||||
|
|
||||||
|
private def it_transforms(number, expected)
|
||||||
|
it "transforms large number #{number}" do
|
||||||
|
s = Ameba::Source.new number
|
||||||
|
Ameba::Rules::LargeNumbers.new.catch(s).should_not be_valid
|
||||||
|
s.errors.first.message.should contain expected
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module Ameba::Rules
|
||||||
|
subject = LargeNumbers.new
|
||||||
|
|
||||||
|
describe LargeNumbers do
|
||||||
|
it "passes if large number does not require underscore" do
|
||||||
|
s = Source.new %q(
|
||||||
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
||||||
|
16 17 18 19 20 30 40 50 60 70 80 90
|
||||||
|
100
|
||||||
|
1_000
|
||||||
|
10_000
|
||||||
|
100_000
|
||||||
|
200_000
|
||||||
|
300_000
|
||||||
|
400_000
|
||||||
|
500_000
|
||||||
|
600_000
|
||||||
|
700_000
|
||||||
|
800_000
|
||||||
|
900_000
|
||||||
|
1_000_000
|
||||||
|
|
||||||
|
-9_223_372_036_854_775_808
|
||||||
|
9_223_372_036_854_775_807
|
||||||
|
|
||||||
|
141_592_654
|
||||||
|
141_592_654.0
|
||||||
|
141_592_654.001
|
||||||
|
141_592_654.001_2
|
||||||
|
141_592_654.001_23
|
||||||
|
141_592_654.001_234
|
||||||
|
141_592_654.001_234_5
|
||||||
|
|
||||||
|
0b1101
|
||||||
|
0o123
|
||||||
|
0xFE012D
|
||||||
|
0xfe012d
|
||||||
|
0xfe012dd11
|
||||||
|
|
||||||
|
1_i8
|
||||||
|
12_i16
|
||||||
|
123_i32
|
||||||
|
1_234_i64
|
||||||
|
|
||||||
|
12_u8
|
||||||
|
123_u16
|
||||||
|
1_234_u32
|
||||||
|
9_223_372_036_854_775_808_u64
|
||||||
|
9_223_372_036_854_775_808.000_123_456_789_f64
|
||||||
|
|
||||||
|
+100_u32
|
||||||
|
-900_000_i32
|
||||||
|
|
||||||
|
1_234.5e-7
|
||||||
|
11_234e10_f32
|
||||||
|
+1.123
|
||||||
|
-0.000_5
|
||||||
|
)
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it_transforms "10000", "10_000"
|
||||||
|
it_transforms "+10000", "+10_000"
|
||||||
|
it_transforms "-10000", "-10_000"
|
||||||
|
|
||||||
|
it_transforms "9223372036854775808", "9_223_372_036_854_775_808"
|
||||||
|
it_transforms "-9223372036854775808", "-9_223_372_036_854_775_808"
|
||||||
|
it_transforms "+9223372036854775808", "+9_223_372_036_854_775_808"
|
||||||
|
|
||||||
|
it_transforms "1_00000", "100_000"
|
||||||
|
|
||||||
|
it_transforms "1_23_i8", "123_i8"
|
||||||
|
it_transforms "1000_i16", "1_000_i16"
|
||||||
|
it_transforms "1000_i32", "1_000_i32"
|
||||||
|
it_transforms "1000_i64", "1_000_i64"
|
||||||
|
|
||||||
|
it_transforms "1_23_u8", "123_u8"
|
||||||
|
it_transforms "1000_u16", "1_000_u16"
|
||||||
|
it_transforms "1000_u32", "1_000_u32"
|
||||||
|
it_transforms "1000_u64", "1_000_u64"
|
||||||
|
|
||||||
|
it_transforms "123456_f32", "123_456_f32"
|
||||||
|
it_transforms "123456_f64", "123_456_f64"
|
||||||
|
|
||||||
|
it_transforms "123456.5e-7_f32", "123_456.5e-7_f32"
|
||||||
|
it_transforms "123456e10_f64", "123_456e10_f64"
|
||||||
|
|
||||||
|
it_transforms "123456.5e-7", "123_456.5e-7"
|
||||||
|
it_transforms "123456e10", "123_456e10"
|
||||||
|
|
||||||
|
it_transforms "3.00_1", "3.001"
|
||||||
|
it_transforms "3.0012", "3.001_2"
|
||||||
|
it_transforms "3.00123", "3.001_23"
|
||||||
|
it_transforms "3.001234", "3.001_234"
|
||||||
|
it_transforms "3.0012345", "3.001_234_5"
|
||||||
|
|
||||||
|
it "reports rule, pos and message" do
|
||||||
|
s = Source.new %q(
|
||||||
|
1200000
|
||||||
|
)
|
||||||
|
subject.catch(s).should_not be_valid
|
||||||
|
error = s.errors.first
|
||||||
|
error.rule.should_not be_nil
|
||||||
|
error.pos.should eq 2
|
||||||
|
error.message.should match /1_200_000/
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -21,15 +21,5 @@ module Ameba
|
||||||
s.errors.first.message.should eq "Error!"
|
s.errors.first.message.should eq "Error!"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#ast" do
|
|
||||||
it "returns ast nodes" do
|
|
||||||
s = Source.new %(
|
|
||||||
class A; end
|
|
||||||
class B; end
|
|
||||||
)
|
|
||||||
s.ast.to_s.should eq "class A\nend\nclass B\nend\n"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
42
spec/ameba/tokenizer_spec.cr
Normal file
42
spec/ameba/tokenizer_spec.cr
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
require "../spec_helper"
|
||||||
|
|
||||||
|
private def it_tokenizes(str, expected)
|
||||||
|
it "tokenizes #{str}" do
|
||||||
|
([] of Symbol).tap do |token_types|
|
||||||
|
Ameba::Tokenizer.new(Ameba::Source.new str).run do |token|
|
||||||
|
token_types << token.type
|
||||||
|
end.should be_true
|
||||||
|
end.should eq expected
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module Ameba
|
||||||
|
describe Tokenizer do
|
||||||
|
describe "#run" do
|
||||||
|
it_tokenizes %("string"), %i(STRING)
|
||||||
|
it_tokenizes %(100), %i(NUMBER)
|
||||||
|
it_tokenizes %('a'), %i(CHAR)
|
||||||
|
it_tokenizes %([]), %i([])
|
||||||
|
it_tokenizes %([] of String), %i([] SPACE IDENT SPACE CONST)
|
||||||
|
it_tokenizes %q("str #{3}"), %i(STRING NUMBER)
|
||||||
|
|
||||||
|
it_tokenizes %(%w(1 2)),
|
||||||
|
%i(STRING_ARRAY_START STRING STRING STRING_ARRAY_END)
|
||||||
|
|
||||||
|
it_tokenizes %(%i(one two)),
|
||||||
|
%i(SYMBOL_ARRAY_START STRING STRING STRING_ARRAY_END)
|
||||||
|
|
||||||
|
it_tokenizes %(
|
||||||
|
class A
|
||||||
|
def method
|
||||||
|
puts "hello"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
), [
|
||||||
|
:NEWLINE, :SPACE, :IDENT, :SPACE, :CONST, :NEWLINE, :SPACE, :IDENT,
|
||||||
|
:SPACE, :IDENT, :NEWLINE, :SPACE, :IDENT, :SPACE, :STRING, :NEWLINE,
|
||||||
|
:SPACE, :IDENT, :NEWLINE, :SPACE, :IDENT, :NEWLINE, :SPACE,
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -16,7 +16,9 @@ module Ameba::AST
|
||||||
@source : Source
|
@source : Source
|
||||||
|
|
||||||
def initialize(@rule, @source)
|
def initialize(@rule, @source)
|
||||||
@source.ast.accept self
|
parser = Crystal::Parser.new(@source.content)
|
||||||
|
parser.filename = @source.path
|
||||||
|
parser.parse.accept self
|
||||||
end
|
end
|
||||||
|
|
||||||
def visit(node : Crystal::ASTNode)
|
def visit(node : Crystal::ASTNode)
|
||||||
|
|
86
src/ameba/rules/large_numbers.cr
Normal file
86
src/ameba/rules/large_numbers.cr
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
module Ameba::Rules
|
||||||
|
# A rule that disallows usage of large numbers without underscore.
|
||||||
|
# These do not affect the value of the number, but can help read
|
||||||
|
# large numbers more easily.
|
||||||
|
#
|
||||||
|
# For example, these are considered invalid:
|
||||||
|
#
|
||||||
|
# ```
|
||||||
|
# 10000
|
||||||
|
# 141592654
|
||||||
|
# 5.12345
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# And has to be rewritten as the following:
|
||||||
|
#
|
||||||
|
# ```
|
||||||
|
# 10_000
|
||||||
|
# 141_592_654
|
||||||
|
# 5.123_45
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
struct LargeNumbers < Rule
|
||||||
|
def test(source)
|
||||||
|
Tokenizer.new(source).run do |token|
|
||||||
|
next unless token.type == :NUMBER && decimal?(token.raw)
|
||||||
|
|
||||||
|
if (expected = underscored token.raw) != token.raw
|
||||||
|
source.error self, token.line_number,
|
||||||
|
"Large numbers should be written with underscores: #{expected}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def decimal?(value)
|
||||||
|
value !~ /^0(x|b|o)/
|
||||||
|
end
|
||||||
|
|
||||||
|
private def underscored(raw_number)
|
||||||
|
sign, value, fraction, suffix = parse_number raw_number
|
||||||
|
value = slice_digits(value.reverse) { |slice| slice }.reverse
|
||||||
|
fraction = "." + slice_digits(fraction) { |slice| slice } if fraction
|
||||||
|
|
||||||
|
"#{sign}#{value}#{fraction}#{suffix}"
|
||||||
|
end
|
||||||
|
|
||||||
|
private def slice_digits(value, by = 3)
|
||||||
|
([] of String).tap do |slices|
|
||||||
|
value.chars.reject(&.== '_').each_slice(by) do |slice|
|
||||||
|
slices << (yield slice).join
|
||||||
|
end
|
||||||
|
end.join("_")
|
||||||
|
end
|
||||||
|
|
||||||
|
private def parse_number(value)
|
||||||
|
value, sign = parse_sign(value)
|
||||||
|
value, suffix = parse_suffix(value)
|
||||||
|
value, fraction = parse_fraction(value)
|
||||||
|
|
||||||
|
{sign, value, fraction, suffix}
|
||||||
|
end
|
||||||
|
|
||||||
|
private def parse_sign(value)
|
||||||
|
if "+-".includes?(value[0])
|
||||||
|
sign = value[0]
|
||||||
|
value = value[1..-1]
|
||||||
|
end
|
||||||
|
{value, sign}
|
||||||
|
end
|
||||||
|
|
||||||
|
private def parse_suffix(value)
|
||||||
|
if pos = (value =~ /e/ || value =~ /_(i|u|f)/)
|
||||||
|
suffix = value[pos..-1]
|
||||||
|
value = value[0..pos - 1]
|
||||||
|
end
|
||||||
|
{value, suffix}
|
||||||
|
end
|
||||||
|
|
||||||
|
private def parse_fraction(value)
|
||||||
|
if comma = value.index('.')
|
||||||
|
fraction = value[comma + 1..-1]
|
||||||
|
value = value[0..comma - 1]
|
||||||
|
end
|
||||||
|
{value, fraction}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -15,7 +15,6 @@ module Ameba
|
||||||
getter errors = [] of Error
|
getter errors = [] of Error
|
||||||
getter path : String?
|
getter path : String?
|
||||||
getter content : String
|
getter content : String
|
||||||
getter ast : Crystal::ASTNode?
|
|
||||||
|
|
||||||
def initialize(@content : String, @path = nil)
|
def initialize(@content : String, @path = nil)
|
||||||
end
|
end
|
||||||
|
@ -31,11 +30,5 @@ module Ameba
|
||||||
def lines
|
def lines
|
||||||
@lines ||= @content.split("\n")
|
@lines ||= @content.split("\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
def ast
|
|
||||||
@ast ||= Crystal::Parser.new(@content)
|
|
||||||
.tap { |p| p.filename = @path }
|
|
||||||
.parse
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
74
src/ameba/tokenizer.cr
Normal file
74
src/ameba/tokenizer.cr
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
require "compiler/crystal/syntax/*"
|
||||||
|
|
||||||
|
module Ameba
|
||||||
|
class Tokenizer
|
||||||
|
def initialize(source)
|
||||||
|
@lexer = Crystal::Lexer.new source.content
|
||||||
|
@lexer.count_whitespace = true
|
||||||
|
@lexer.comments_enabled = true
|
||||||
|
@lexer.wants_raw = true
|
||||||
|
@lexer.filename = source.path
|
||||||
|
end
|
||||||
|
|
||||||
|
def run(&block : Crystal::Token -> _)
|
||||||
|
run_normal_state @lexer, &block
|
||||||
|
true
|
||||||
|
rescue e : Crystal::SyntaxException
|
||||||
|
# puts e
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
private def run_normal_state(lexer, break_on_rcurly = false,
|
||||||
|
&block : Crystal::Token -> _)
|
||||||
|
while true
|
||||||
|
token = @lexer.next_token
|
||||||
|
case token.type
|
||||||
|
when :DELIMITER_START
|
||||||
|
run_delimiter_state lexer, token, &block
|
||||||
|
when :STRING_ARRAY_START, :SYMBOL_ARRAY_START
|
||||||
|
block.call token
|
||||||
|
run_array_state lexer, token, &block
|
||||||
|
when :EOF
|
||||||
|
break
|
||||||
|
when :"}"
|
||||||
|
break if break_on_rcurly
|
||||||
|
block.call token
|
||||||
|
else
|
||||||
|
block.call token
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def run_delimiter_state(lexer, token, &block : Crystal::Token -> _)
|
||||||
|
while true
|
||||||
|
token = @lexer.next_string_token(token.delimiter_state)
|
||||||
|
case token.type
|
||||||
|
when :DELIMITER_END
|
||||||
|
break
|
||||||
|
when :INTERPOLATION_START
|
||||||
|
run_normal_state lexer, break_on_rcurly: true, &block
|
||||||
|
when :EOF
|
||||||
|
break
|
||||||
|
else
|
||||||
|
block.call token
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def run_array_state(lexer, token, &block : Crystal::Token -> _)
|
||||||
|
while true
|
||||||
|
lexer.next_string_array_token
|
||||||
|
|
||||||
|
case token.type
|
||||||
|
when :STRING_ARRAY_END
|
||||||
|
block.call token
|
||||||
|
break
|
||||||
|
when :EOF
|
||||||
|
raise "Unterminated symbol array literal"
|
||||||
|
else
|
||||||
|
block.call token
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue