From a94a823f98d3d94915f594fbe5abbd0bc5c62a08 Mon Sep 17 00:00:00 2001 From: Cristian Molina Date: Sun, 7 Mar 2021 08:32:45 -0300 Subject: [PATCH 01/33] Fix SDL examples crashes (#10470) --- samples/sdl/fire.cr | 2 +- samples/sdl/sdl/surface.cr | 2 +- samples/sdl/tv.cr | 46 +++++++++++++++++++++----------------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/samples/sdl/fire.cr b/samples/sdl/fire.cr index 998b72443d8a..ef88a663a065 100644 --- a/samples/sdl/fire.cr +++ b/samples/sdl/fire.cr @@ -281,7 +281,7 @@ class Screen end def put_pixel(point, color) - color = color.to_u32 + color = color.to_u32! offset = @surface.offset(point.x, point.y) @surface[offset] = color diff --git a/samples/sdl/sdl/surface.cr b/samples/sdl/sdl/surface.cr index 0a72483517c0..d2cf4c15006a 100644 --- a/samples/sdl/sdl/surface.cr +++ b/samples/sdl/sdl/surface.cr @@ -24,7 +24,7 @@ class SDL::Surface end def []=(offset, color) - (@surface.value.pixels.as(UInt32*))[offset] = color.to_u32 + (@surface.value.pixels.as(UInt32*))[offset] = color.to_u32! end def []=(x, y, color) diff --git a/samples/sdl/tv.cr b/samples/sdl/tv.cr index eb658fc758a8..0cd491113074 100644 --- a/samples/sdl/tv.cr +++ b/samples/sdl/tv.cr @@ -130,34 +130,38 @@ color_maker = ColorMaker.new(delay) rects = parse_rectangles puts "Rects: #{rects.size}" -while true - SDL.poll_events do |event| - if event.type == LibSDL::QUIT || event.type == LibSDL::KEYDOWN - ms = SDL.ticks - start - puts "#{frames} frames in #{ms} ms" - puts "Average FPS: #{frames / (ms * 0.001)}" - SDL.quit - exit +begin + while true + SDL.poll_events do |event| + if event.type == LibSDL::QUIT || event.type == LibSDL::KEYDOWN + ms = SDL.ticks - start + puts "#{frames} frames in #{ms} ms" + puts "Average FPS: #{frames / (ms * 0.001)}" + SDL.quit + exit + end end - end - surface.lock + surface.lock - (height // 10).times do |h| - (width // 10).times do |w| - rect = rects.find { |rect| rect.contains?(w, h) } - 10.times do |y| - 10.times do |x| - surface[x + 10 * w, y + 10 * h] = rect ? (rect.light? ? color_maker.light_color : color_maker.dark_color) : color_maker.black_color + (height // 10).times do |h| + (width // 10).times do |w| + rect = rects.find { |rect| rect.contains?(w, h) } + 10.times do |y| + 10.times do |x| + surface[x + 10 * w, y + 10 * h] = rect ? (rect.light? ? color_maker.light_color : color_maker.dark_color) : color_maker.black_color + end end end end - end - color_maker.next + color_maker.next - surface.unlock - surface.flip + surface.unlock + surface.flip - frames += 1 + frames += 1 + end +ensure + SDL.quit end From 5878c876a3b4c5de3e4ae8cc64af77511d57f232 Mon Sep 17 00:00:00 2001 From: Kenichi Kamiya Date: Sun, 7 Mar 2021 23:45:10 +0900 Subject: [PATCH 02/33] Correct expectation to ensure set identifier (#10482) --- spec/std/set_spec.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/std/set_spec.cr b/spec/std/set_spec.cr index c83c704daba5..73502de32601 100644 --- a/spec/std/set_spec.cr +++ b/spec/std/set_spec.cr @@ -36,7 +36,7 @@ describe "Set" do it "returns self" do set = Set(Int32).new - set.add(1).should eq(set) + set.add(1).should be(set) end end @@ -129,7 +129,7 @@ describe "Set" do it "returns self" do set = Set{1, 4, 8} - set.concat([1, 9, 10]).should eq(Set{1, 4, 8, 9, 10}) + set.concat([1, 9, 10]).should be(set) end end From 9e23b28594ae6849f39263b86447807e4f228b21 Mon Sep 17 00:00:00 2001 From: Hugo Parente Lima Date: Tue, 9 Mar 2021 17:58:41 -0300 Subject: [PATCH 03/33] Use class instead of struct for objects in XML module. (#10436) This will allow a future change to let the XML module manage the libXML2 memory, fixing https://github.com/crystal-lang/crystal/issues/10435 without breaking the API. --- src/xml/attributes.cr | 2 +- src/xml/builder.cr | 2 +- src/xml/namespace.cr | 2 +- src/xml/node.cr | 2 +- src/xml/node/type.cr | 2 +- src/xml/node_set.cr | 2 +- src/xml/reader.cr | 2 +- src/xml/reader/type.cr | 2 +- src/xml/xpath_context.cr | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/xml/attributes.cr b/src/xml/attributes.cr index 10fd2b558e22..5b25fd33c29a 100644 --- a/src/xml/attributes.cr +++ b/src/xml/attributes.cr @@ -1,6 +1,6 @@ require "./node" -struct XML::Attributes +class XML::Attributes include Enumerable(Node) def initialize(@node : Node) diff --git a/src/xml/builder.cr b/src/xml/builder.cr index 4597314ed99d..fecc9e50d119 100644 --- a/src/xml/builder.cr +++ b/src/xml/builder.cr @@ -4,7 +4,7 @@ # an invalid XML (for example, if invoking `end_element` # without a matching `start_element`, or trying to use # a non-string value as an object's field name) -struct XML::Builder +class XML::Builder private CDATA_END = "]]>" private CDATA_ESCAPE = "]]]]>" diff --git a/src/xml/namespace.cr b/src/xml/namespace.cr index 5bcd22995fa8..95b0115170fb 100644 --- a/src/xml/namespace.cr +++ b/src/xml/namespace.cr @@ -1,4 +1,4 @@ -struct XML::Namespace +class XML::Namespace getter document : Node def initialize(@document : Node, @ns : LibXML::NS*) diff --git a/src/xml/node.cr b/src/xml/node.cr index bed80378d35a..4ec9540b711e 100644 --- a/src/xml/node.cr +++ b/src/xml/node.cr @@ -1,4 +1,4 @@ -struct XML::Node +class XML::Node LOOKS_LIKE_XPATH = /^(\.\/|\/|\.\.|\.$)/ # Creates a new node. diff --git a/src/xml/node/type.cr b/src/xml/node/type.cr index 32d0d0c69da6..1945b756477d 100644 --- a/src/xml/node/type.cr +++ b/src/xml/node/type.cr @@ -1,4 +1,4 @@ -struct XML::Node +class XML::Node enum Type NONE = 0 ELEMENT_NODE = 1 diff --git a/src/xml/node_set.cr b/src/xml/node_set.cr index 33b7afbf2d8a..7d26f5520b82 100644 --- a/src/xml/node_set.cr +++ b/src/xml/node_set.cr @@ -1,4 +1,4 @@ -struct XML::NodeSet +class XML::NodeSet include Enumerable(Node) def initialize(@doc : Node, @set : LibXML::NodeSet*) diff --git a/src/xml/reader.cr b/src/xml/reader.cr index b5f8e8af42cd..f48873bdedce 100644 --- a/src/xml/reader.cr +++ b/src/xml/reader.cr @@ -1,7 +1,7 @@ require "./libxml2" require "./parser_options" -struct XML::Reader +class XML::Reader # Creates a new reader from a string. # # See `XML::ParserOptions.default` for default options. diff --git a/src/xml/reader/type.cr b/src/xml/reader/type.cr index 50c156a08143..6c618745773d 100644 --- a/src/xml/reader/type.cr +++ b/src/xml/reader/type.cr @@ -1,4 +1,4 @@ -struct XML::Reader +class XML::Reader enum Type NONE = 0 ELEMENT = 1 diff --git a/src/xml/xpath_context.cr b/src/xml/xpath_context.cr index f6509237e023..e416f67c931d 100644 --- a/src/xml/xpath_context.cr +++ b/src/xml/xpath_context.cr @@ -1,4 +1,4 @@ -struct XML::XPathContext +class XML::XPathContext def initialize(node : Node) @ctx = LibXML.xmlXPathNewContext(node.to_unsafe.value.doc) @ctx.value.node = node.to_unsafe From bd1d44c2e82ad11aae0826fe0853d2a645cd9f4f Mon Sep 17 00:00:00 2001 From: "Brian J. Cardiff" Date: Tue, 9 Mar 2021 17:59:22 -0300 Subject: [PATCH 04/33] Drop deprecated definitions (#10386) * Drop deprecated spec expectations * Drop deprecated Int, String, Time overloads with trailing IO parameter * Drop deprecated Set methods * Drop deprecated YAML.new * Drop deprecated Dir.rmdir, File::Info#owner, File::Info#group * Drop deprecated HTTP::Request, HTTP::WebSocket, HTTP::LogHandler methods * Drop deprecated URI#full_path * Drop deprecated Time::Span#duration * Drop deprecated StaticArray#[]= * Drop deprecated Hash#delete_if * Drop deprecated Process#kill * Drop deprecated OptionParser.parse! * Drop deprecated Log.setup_from_env overload * Drop deprecated has_attribute? macro --- spec/compiler/codegen/macro_spec.cr | 8 +- .../crystal/tools/doc/doc_renderer_spec.cr | 2 +- spec/std/compress/zlib/stress_spec.cr | 2 +- spec/std/http/client/client_spec.cr | 6 +- spec/std/http/request_spec.cr | 4 +- .../http/server/handlers/log_handler_spec.cr | 16 ---- spec/std/int_spec.cr | 3 - spec/std/set_spec.cr | 56 ------------- spec/std/string_spec.cr | 21 ----- spec/std/time/span_spec.cr | 3 +- spec/std/uri_spec.cr | 15 ---- spec/std/yaml/builder_spec.cr | 12 --- src/compiler/crystal/macros.cr | 7 -- src/compiler/crystal/macros/methods.cr | 13 ---- src/dir.cr | 6 -- src/file/info.cr | 12 --- src/hash.cr | 19 ----- src/http/request.cr | 9 --- src/http/server/handlers/log_handler.cr | 5 -- src/http/web_socket.cr | 5 -- src/int.cr | 10 --- src/log/setup.cr | 8 -- src/option_parser.cr | 10 --- src/process.cr | 14 ---- src/set.cr | 20 ----- src/spec/expectations.cr | 10 --- src/static_array.cr | 14 ---- src/string.cr | 78 ------------------- src/time.cr | 8 -- src/time/span.cr | 6 -- src/uri.cr | 6 -- src/yaml/builder.cr | 8 -- 32 files changed, 12 insertions(+), 404 deletions(-) diff --git a/spec/compiler/codegen/macro_spec.cr b/spec/compiler/codegen/macro_spec.cr index 2c511e8eb3f8..0360d05fc83b 100644 --- a/spec/compiler/codegen/macro_spec.cr +++ b/spec/compiler/codegen/macro_spec.cr @@ -770,7 +770,7 @@ describe "Code gen: macro" do )).to_string.should eq("Green") end - it "says that enum has Flags attribute" do + it "says that enum has Flags annotation" do run(%( @[Flags] enum Color @@ -779,11 +779,11 @@ describe "Code gen: macro" do Blue end - {{Color.has_attribute?("Flags")}} + {{Color.annotation(Flags) ? true : false}} )).to_b.should be_true end - it "says that enum doesn't have Flags attribute" do + it "says that enum doesn't have Flags annotation" do run(%( enum Color Red @@ -791,7 +791,7 @@ describe "Code gen: macro" do Blue end - {{Color.has_attribute?("Flags")}} + {{Color.annotation(Flags) ? true : false}} )).to_b.should be_false end diff --git a/spec/compiler/crystal/tools/doc/doc_renderer_spec.cr b/spec/compiler/crystal/tools/doc/doc_renderer_spec.cr index 5ca6ad4be651..4938060e24b2 100644 --- a/spec/compiler/crystal/tools/doc/doc_renderer_spec.cr +++ b/spec/compiler/crystal/tools/doc/doc_renderer_spec.cr @@ -15,7 +15,7 @@ private def it_renders(context, input, output, file = __FILE__, line = __LINE__) generator.type(program) end Doc::Markdown.parse input, Doc::Markdown::DocRenderer.new(c, io) - end.should eq(output), file, line + end.should eq(output), file: file, line: line end end diff --git a/spec/std/compress/zlib/stress_spec.cr b/spec/std/compress/zlib/stress_spec.cr index 4edd527e2e7b..b2be8e229a13 100644 --- a/spec/std/compress/zlib/stress_spec.cr +++ b/spec/std/compress/zlib/stress_spec.cr @@ -5,7 +5,7 @@ module Compress::Zlib describe Zlib do it "write read should be inverse with random string" do expected = String.build do |io| - 1_000_000.times { rand(2000).to_i.to_s(32, io) } + 1_000_000.times { rand(2000).to_i.to_s(io, 32) } end io = IO::Memory.new diff --git a/spec/std/http/client/client_spec.cr b/spec/std/http/client/client_spec.cr index f62e9aa4a368..43ebd7a1c1d0 100644 --- a/spec/std/http/client/client_spec.cr +++ b/spec/std/http/client/client_spec.cr @@ -264,11 +264,11 @@ module HTTP client = TestClient.new "www.example.com" request = HTTP::Request.new("GET", "/") client.set_defaults(request) - request.host.should eq "www.example.com" + request.hostname.should eq "www.example.com" request = HTTP::Request.new("GET", "/", HTTP::Headers{"Host" => "other.example.com"}) client.set_defaults(request) - request.host.should eq "other.example.com" + request.hostname.should eq "other.example.com" end end @@ -288,7 +288,7 @@ module HTTP io_request.rewind request = HTTP::Request.from_io(io_request).as(HTTP::Request) - request.host.should eq("") + request.hostname.should eq("") end it "can specify host and port when initialized with IO" do diff --git a/spec/std/http/request_spec.cr b/spec/std/http/request_spec.cr index 7c45004bae46..78e6b72994f2 100644 --- a/spec/std/http/request_spec.cr +++ b/spec/std/http/request_spec.cr @@ -439,9 +439,9 @@ module HTTP request.query_params.to_s.should eq(new_query) end - it "gets request host from the headers" do + it "gets request hostname from the headers" do request = Request.from_io(IO::Memory.new("GET / HTTP/1.1\r\nHost: host.example.org:3000\r\nReferer:\r\n\r\n")).as(Request) - request.host.should eq("host.example.org") + request.hostname.should eq("host.example.org") end it "#hostname" do diff --git a/spec/std/http/server/handlers/log_handler_spec.cr b/spec/std/http/server/handlers/log_handler_spec.cr index c2a15c385626..3840d3a6eb71 100644 --- a/spec/std/http/server/handlers/log_handler_spec.cr +++ b/spec/std/http/server/handlers/log_handler_spec.cr @@ -39,22 +39,6 @@ describe HTTP::LogHandler do logs.check(:info, %r(^- - GET / HTTP/1.1 - 200 \(\d+(\.\d+)?[mµn]s\)$)) end - it "logs to io" do - request = HTTP::Request.new("GET", "/") - response = HTTP::Server::Response.new(IO::Memory.new) - context = HTTP::Server::Context.new(request, response) - - backend = Log::MemoryBackend.new - io = IO::Memory.new - handler = HTTP::LogHandler.new(io) - handler.next = ->(ctx : HTTP::Server::Context) {} - handler.call(context) - - retry do - io.to_s.should match(%r(- - GET / HTTP/1.1 - 200 \(\d+(\.\d+)?[mµn]s\)$)) - end - end - it "log failed request" do io = IO::Memory.new request = HTTP::Request.new("GET", "/") diff --git a/spec/std/int_spec.cr b/spec/std/int_spec.cr index 93b6db182998..2f765ac13461 100644 --- a/spec/std/int_spec.cr +++ b/spec/std/int_spec.cr @@ -9,8 +9,6 @@ end private def to_s_with_io(num, base, upcase = false) String.build { |io| num.to_s(io, base, upcase: upcase) } - # Test deprecated overload: - String.build { |io| num.to_s(base, io, upcase) } end describe "Int" do @@ -176,7 +174,6 @@ describe "Int" do it { 1234.to_s(36).should eq("ya") } it { -1234.to_s(36).should eq("-ya") } it { 1234.to_s(16, upcase: true).should eq("4D2") } - it { 1234.to_s(16, true).should eq("4D2") } # Deprecated test it { -1234.to_s(16, upcase: true).should eq("-4D2") } it { 1234.to_s(36, upcase: true).should eq("YA") } it { -1234.to_s(36, upcase: true).should eq("-YA") } diff --git a/spec/std/set_spec.cr b/spec/std/set_spec.cr index 73502de32601..43401365a3e4 100644 --- a/spec/std/set_spec.cr +++ b/spec/std/set_spec.cr @@ -340,20 +340,6 @@ describe "Set" do iter.next.should be_a(Iterator::Stop) end - it "check subset" do - set = Set{1, 2, 3} - empty_set = Set(Int32).new - - set.subset?(Set{1, 2, 3, 4}).should be_true - set.subset?(Set{1, 2, 3, "4"}).should be_true - set.subset?(Set{1, 2, 3}).should be_true - set.subset?(Set{1, 2}).should be_false - set.subset?(empty_set).should be_false - - empty_set.subset?(Set{1}).should be_true - empty_set.subset?(empty_set).should be_true - end - it "#subset_of?" do set = Set{1, 2, 3} empty_set = Set(Int32).new @@ -368,20 +354,6 @@ describe "Set" do empty_set.subset_of?(empty_set).should be_true end - it "check proper_subset" do - set = Set{1, 2, 3} - empty_set = Set(Int32).new - - set.proper_subset?(Set{1, 2, 3, 4}).should be_true - set.proper_subset?(Set{1, 2, 3, "4"}).should be_true - set.proper_subset?(Set{1, 2, 3}).should be_false - set.proper_subset?(Set{1, 2}).should be_false - set.proper_subset?(empty_set).should be_false - - empty_set.proper_subset?(Set{1}).should be_true - empty_set.proper_subset?(empty_set).should be_false - end - it "#proper_subset_of?" do set = Set{1, 2, 3} empty_set = Set(Int32).new @@ -396,20 +368,6 @@ describe "Set" do empty_set.proper_subset_of?(empty_set).should be_false end - it "check superset" do - set = Set{1, 2, "3"} - empty_set = Set(Int32).new - - set.superset?(empty_set).should be_true - set.superset?(Set{1, 2}).should be_true - set.superset?(Set{1, 2, "3"}).should be_true - set.superset?(Set{1, 2, 3}).should be_false - set.superset?(Set{1, 2, 3, 4}).should be_false - set.superset?(Set{1, 4}).should be_false - - empty_set.superset?(empty_set).should be_true - end - it "#superset_of?" do set = Set{1, 2, "3"} empty_set = Set(Int32).new @@ -424,20 +382,6 @@ describe "Set" do empty_set.superset_of?(empty_set).should be_true end - it "check proper_superset" do - set = Set{1, 2, "3"} - empty_set = Set(Int32).new - - set.proper_superset?(empty_set).should be_true - set.proper_superset?(Set{1, 2}).should be_true - set.proper_superset?(Set{1, 2, "3"}).should be_false - set.proper_superset?(Set{1, 2, 3}).should be_false - set.proper_superset?(Set{1, 2, 3, 4}).should be_false - set.proper_superset?(Set{1, 4}).should be_false - - empty_set.proper_superset?(empty_set).should be_false - end - it "#proper_superset_of?" do set = Set{1, 2, "3"} empty_set = Set(Int32).new diff --git a/spec/std/string_spec.cr b/spec/std/string_spec.cr index 6e6e363eba05..0d21f3bd8220 100644 --- a/spec/std/string_spec.cr +++ b/spec/std/string_spec.cr @@ -2193,13 +2193,6 @@ describe "String" do it { String.build { |io| "12".ljust(io, 7, '-') }.should eq("12-----") } it { String.build { |io| "12".ljust(io, 7, 'あ') }.should eq("12あああああ") } end - - describe "to io (deprecated)" do - it { String.build { |io| "123".ljust(2, io) }.should eq("123") } - it { String.build { |io| "123".ljust(5, io) }.should eq("123 ") } - it { String.build { |io| "12".ljust(7, '-', io) }.should eq("12-----") } - it { String.build { |io| "12".ljust(7, 'あ', io) }.should eq("12あああああ") } - end end describe "rjust" do @@ -2214,13 +2207,6 @@ describe "String" do it { String.build { |io| "12".rjust(io, 7, '-') }.should eq("-----12") } it { String.build { |io| "12".rjust(io, 7, 'あ') }.should eq("あああああ12") } end - - describe "to io (deprecated)" do - it { String.build { |io| "123".rjust(2, io) }.should eq("123") } - it { String.build { |io| "123".rjust(5, io) }.should eq(" 123") } - it { String.build { |io| "12".rjust(7, '-', io) }.should eq("-----12") } - it { String.build { |io| "12".rjust(7, 'あ', io) }.should eq("あああああ12") } - end end describe "center" do @@ -2235,13 +2221,6 @@ describe "String" do it { String.build { |io| "12".center(io, 7, '-') }.should eq("--12---") } it { String.build { |io| "12".center(io, 7, 'あ') }.should eq("ああ12あああ") } end - - describe "to io (deprecated)" do - it { String.build { |io| "123".center(2, io) }.should eq("123") } - it { String.build { |io| "123".center(5, io) }.should eq(" 123 ") } - it { String.build { |io| "12".center(7, '-', io) }.should eq("--12---") } - it { String.build { |io| "12".center(7, 'あ', io) }.should eq("ああ12あああ") } - end end describe "succ" do diff --git a/spec/std/time/span_spec.cr b/spec/std/time/span_spec.cr index 8976872af00c..ca3eb40f35f8 100644 --- a/spec/std/time/span_spec.cr +++ b/spec/std/time/span_spec.cr @@ -173,9 +173,8 @@ describe Time::Span do 1_000_000.5.days.to_s.should eq("1000000.12:00:00") end - it "test negate and duration" do + it "test negate and abs" do (-Time::Span.new(nanoseconds: 1234500)).to_s.should eq("-00:00:00.001234500") - Time::Span.new(nanoseconds: -1234500).duration.to_s.should eq("00:00:00.001234500") Time::Span.new(nanoseconds: -1234500).abs.to_s.should eq("00:00:00.001234500") (-Time::Span.new(nanoseconds: 7700)).to_s.should eq("-00:00:00.000007700") (+Time::Span.new(nanoseconds: 7700)).to_s.should eq("00:00:00.000007700") diff --git a/spec/std/uri_spec.cr b/spec/std/uri_spec.cr index 071ba01b3929..a2c973548d7b 100644 --- a/spec/std/uri_spec.cr +++ b/spec/std/uri_spec.cr @@ -204,21 +204,6 @@ describe "URI" do it { URI.new(scheme: "scheme", path: "/path").authority.should be_nil } end - describe "#full_path" do - it { URI.new(path: "/foo").full_path.should eq("/foo") } - it { URI.new.full_path.should eq("/") } - it { URI.new(path: "/foo", query: "q=1").full_path.should eq("/foo?q=1") } - it { URI.new(path: "/", query: "q=1").full_path.should eq("/?q=1") } - it { URI.new(query: "q=1").full_path.should eq("/?q=1") } - it { URI.new(path: "/a%3Ab").full_path.should eq("/a%3Ab") } - - it "does not add '?' to the end if the query params are empty" do - uri = URI.parse("http://www.example.com/foo") - uri.query = "" - uri.full_path.should eq("/foo") - end - end - describe "#request_target" do it { URI.new(path: "/foo").request_target.should eq("/foo") } it { URI.new.request_target.should eq("/") } diff --git a/spec/std/yaml/builder_spec.cr b/spec/std/yaml/builder_spec.cr index 5c225aa64b91..b16cab6d0aeb 100644 --- a/spec/std/yaml/builder_spec.cr +++ b/spec/std/yaml/builder_spec.cr @@ -190,16 +190,4 @@ describe YAML::Builder do end end.should eq 1.to_yaml end - - it ".new (with block)" do - String.build do |io| - YAML::Builder.new(io) do |builder| - builder.stream do - builder.document do - builder.scalar(1) - end - end - end - end.should eq 1.to_yaml - end end diff --git a/src/compiler/crystal/macros.cr b/src/compiler/crystal/macros.cr index 0799c0ac8c28..403023025e08 100644 --- a/src/compiler/crystal/macros.cr +++ b/src/compiler/crystal/macros.cr @@ -1919,13 +1919,6 @@ module Crystal::Macros def has_method?(name : StringLiteral | SymbolLiteral) : BoolLiteral end - # Returns `true` if this type has an attribute. For example `@[Flags]` - # or `@[Packed]` (the name you pass to this method is `"Flags"` or `"Packed"` - # in these cases). - @[Deprecated("Use #annotation instead")] - def has_attribute?(name : StringLiteral | SymbolLiteral) : BoolLiteral - end - # Returns the last `Annotation` with the given `type` # attached to this variable or `NilLiteral` if there are none. def annotation(type : TypeNode) : Annotation | NilLiteral diff --git a/src/compiler/crystal/macros/methods.cr b/src/compiler/crystal/macros/methods.cr index df77a7dec657..36ef41f37e6d 100644 --- a/src/compiler/crystal/macros/methods.cr +++ b/src/compiler/crystal/macros/methods.cr @@ -1624,19 +1624,6 @@ module Crystal value = arg.to_string("argument to 'TypeNode#has_method?'") TypeNode.has_method?(type, value) end - when "has_attribute?" - interpreter.report_warning_at(name_loc, "Deprecated TypeNode#has_attribute?. Use #annotation instead") - interpret_one_arg_method(method, args) do |arg| - value = arg.to_string("argument to 'TypeNode#has_attribute?'") - case value - when "Flags" - BoolLiteral.new(!!type.as?(EnumType).try &.flags?) - when "Packed" - BoolLiteral.new(!!type.as?(ClassType).try &.packed?) - else - BoolLiteral.new(false) - end - end when "annotation" fetch_annotation(self, method, args) do |type| self.type.annotation(type) diff --git a/src/dir.cr b/src/dir.cr index cac0382f88f9..5cb49c7d4814 100644 --- a/src/dir.cr +++ b/src/dir.cr @@ -248,12 +248,6 @@ class Dir mkdir(path, mode) unless Dir.exists?(path) end - # Removes the directory at the given path. - @[Deprecated("Use `Dir.delete` instead")] - def self.rmdir(path : Path | String) - delete(path) - end - # Removes the directory at the given path. def self.delete(path : Path | String) Crystal::System::Dir.delete(path.to_s) diff --git a/src/file/info.cr b/src/file/info.cr index 40a12f32e4e3..c12a0ed6471d 100644 --- a/src/file/info.cr +++ b/src/file/info.cr @@ -96,21 +96,9 @@ class File # The user ID that the file belongs to. abstract def owner_id : String - # :ditto: - @[Deprecated("Use File#owner_id instead")] - def owner : UInt32 - owner_id.to_u32 - end - # The group ID that the file belongs to. abstract def group_id : String - # :ditto: - @[Deprecated("Use File#group_id instead")] - def group : UInt32 - group_id.to_u32 - end - # Returns true if this `Info` and *other* are of the same file. # # On unix, this compares device and inode fields, and will compare equal for diff --git a/src/hash.cr b/src/hash.cr index 19a059969afe..acf4a45d234d 100644 --- a/src/hash.cr +++ b/src/hash.cr @@ -1208,25 +1208,6 @@ class Hash(K, V) entry ? entry.value : yield key end - # Deletes each key-value pair for which the given block returns `true`. - # - # ``` - # h = {"foo" => "bar", "fob" => "baz", "bar" => "qux"} - # h.delete_if { |key, value| key.starts_with?("fo") } - # h # => { "bar" => "qux" } - # ``` - @[Deprecated("Use `#reject!` instead")] - def delete_if - keys_to_delete = [] of K - each do |key, value| - keys_to_delete << key if yield(key, value) - end - keys_to_delete.each do |key| - delete(key) - end - self - end - # Returns `true` when hash contains no key-value pairs. # # ``` diff --git a/src/http/request.cr b/src/http/request.cr index a300f3a6cd01..b2f2976229c4 100644 --- a/src/http/request.cr +++ b/src/http/request.cr @@ -259,15 +259,6 @@ class HTTP::Request value end - # Returns request host from headers. - @[Deprecated("Use `#hostname` instead.")] - def host - host = @headers["Host"]? - return unless host - index = host.index(":") - index ? host[0...index] : host - end - # Extracts the hostname from `Host` header. # # Returns `nil` if the `Host` header is missing. diff --git a/src/http/server/handlers/log_handler.cr b/src/http/server/handlers/log_handler.cr index 43c9af150c3e..36d1c90c7a91 100644 --- a/src/http/server/handlers/log_handler.cr +++ b/src/http/server/handlers/log_handler.cr @@ -5,11 +5,6 @@ require "log" class HTTP::LogHandler include HTTP::Handler - @[Deprecated("Use `new([Log])` instead")] - def initialize(io : IO) - @log = Log.new("http.server", Log::IOBackend.new(io), :info) - end - def initialize(@log = Log.for("http.server")) end diff --git a/src/http/web_socket.cr b/src/http/web_socket.cr index 4d72a4babdb3..827c1d87c1b9 100644 --- a/src/http/web_socket.cr +++ b/src/http/web_socket.cr @@ -103,11 +103,6 @@ class HTTP::WebSocket end end - @[Deprecated("Use WebSocket#close(code, message) instead")] - def close(message) - close(nil, message) - end - # Sends a close frame to the client, and closes the connection. # The close frame may contain a body (message) that indicates the reason for closing. def close(code : CloseCode | Int? = nil, message = nil) diff --git a/src/int.cr b/src/int.cr index 2dd86031e8a7..3e3781370fae 100644 --- a/src/int.cr +++ b/src/int.cr @@ -630,11 +630,6 @@ struct Int end end - @[Deprecated("Use `#to_s(base : Int, *, upcase : Bool = false)` instead")] - def to_s(base : Int, _upcase : Bool) : String - to_s(base, upcase: _upcase) - end - def to_s(io : IO, base : Int = 10, *, upcase : Bool = false) : Nil raise ArgumentError.new("Invalid base #{base}") unless 2 <= base <= 36 || base == 62 raise ArgumentError.new("upcase must be false for base 62") if upcase && base == 62 @@ -651,11 +646,6 @@ struct Int end end - @[Deprecated("Use `#to_s(io : IO, base : Int, *, upcase : Bool = false)` instead")] - def to_s(base : Int, io : IO, upcase : Bool = false) : Nil - to_s(io, base, upcase: upcase) - end - private def internal_to_s(base, upcase = false) # Given sizeof(self) <= 128 bits, we need at most 128 bytes for a base 2 # representation, plus one byte for the trailing 0. diff --git a/src/log/setup.cr b/src/log/setup.cr index cb26c44d5962..7371f8876b40 100644 --- a/src/log/setup.cr +++ b/src/log/setup.cr @@ -26,14 +26,6 @@ class Log end end - @[Deprecated("Use default_level, default_sources named arguments")] - def self.setup_from_env(*, builder : Log::Builder = Log.builder, - level : String, - sources : String, - backend = Log::IOBackend.new) - Log.setup(sources, Log::Severity.parse(level), backend, builder: builder) - end - # Setups logging based on `LOG_LEVEL` environment variable. def self.setup_from_env(*, builder : Log::Builder = Log.builder, default_level : Log::Severity = Log::Severity::Info, diff --git a/src/option_parser.cr b/src/option_parser.cr index 4f483843ab21..90a63d0ffa4a 100644 --- a/src/option_parser.cr +++ b/src/option_parser.cr @@ -115,11 +115,6 @@ class OptionParser parser end - @[Deprecated("Use `parse` instead.")] - def self.parse! : self - parse(ARGV) { |parser| yield parser } - end - # Creates a new parser. def initialize @flags = [] of String @@ -447,9 +442,4 @@ class OptionParser end end end - - @[Deprecated("Use `parse` instead.")] - def parse! - parse - end end diff --git a/src/process.cr b/src/process.cr index c679205b0538..3c83e25d02a3 100644 --- a/src/process.cr +++ b/src/process.cr @@ -41,14 +41,6 @@ class Process Crystal::System::Process.ppid.to_i64 end - # Sends a *signal* to the processes identified by the given *pids*. - @[Deprecated("Use #signal instead")] - def self.kill(signal : Signal, *pids : Int) - pids.each do |pid| - signal(signal, pid) - end - end - # Sends *signal* to the process identified by *pid*. def self.signal(signal : Signal, pid : Int) : Nil Crystal::System::Process.signal(pid, signal.value) @@ -296,12 +288,6 @@ class Process @process_info = Crystal::System::Process.new(pid) end - # See also: `Process.kill` - @[Deprecated("Use #signal instead")] - def kill(sig = Signal::TERM) - signal sig - end - # Sends *signal* to this process. def signal(signal : Signal) Crystal::System::Process.signal(@process_info.pid, signal) diff --git a/src/set.cr b/src/set.cr index d9904d0d6469..b44fadaaf9fe 100644 --- a/src/set.cr +++ b/src/set.cr @@ -430,11 +430,6 @@ struct Set(T) all? { |value| other.includes?(value) } end - @[Deprecated("Use #subset_of? instead.")] - def subset?(other : Set) - subset_of?(other) - end - # Returns `true` if the set is a proper subset of the *other* set. # # This set must have fewer elements than the *other* set, and all @@ -449,11 +444,6 @@ struct Set(T) all? { |value| other.includes?(value) } end - @[Deprecated("Use #proper_subset_of? instead.")] - def proper_subset?(other : Set) - proper_subset_of?(other) - end - # Returns `true` if the set is a superset of the *other* set. # # The *other* must have the same or fewer elements than this set, and all of @@ -467,11 +457,6 @@ struct Set(T) other.subset_of?(self) end - @[Deprecated("Use #superset_of? instead.")] - def superset?(other : Set) - superset_of?(other) - end - # Returns `true` if the set is a superset of the *other* set. # # The *other* must have the same or fewer elements than this set, and all of @@ -485,11 +470,6 @@ struct Set(T) other.proper_subset_of?(self) end - @[Deprecated("Use #proper_superset_of? instead.")] - def proper_superset?(other : Set) - proper_superset_of?(other) - end - # :nodoc: def object_id @hash.object_id diff --git a/src/spec/expectations.cr b/src/spec/expectations.cr index 07b8c9337c39..8bd2a4f2d88c 100644 --- a/src/spec/expectations.cr +++ b/src/spec/expectations.cr @@ -502,16 +502,6 @@ module Spec fail(failure_message, file, line) end end - - @[Deprecated("Use named arguments `.should(expectation, file: file, line: line)`")] - def should(expectation, _file, _line) - should(expectation, file: _file, line: _line) - end - - @[Deprecated("Use named arguments `.should_not(expectation, file: file, line: line)`")] - def should_not(expectation, _file, _line) - should_not(expectation, file: _file, line: _line) - end end end diff --git a/src/static_array.cr b/src/static_array.cr index 07c35ad9ba9f..31a1a9fa3eae 100644 --- a/src/static_array.cr +++ b/src/static_array.cr @@ -193,20 +193,6 @@ struct StaticArray(T, N) self end - # Fills the array by substituting all elements with the given value. - # - # ``` - # array = StaticArray(Int32, 3).new { |i| i + 1 } - # array.[]= 2 # => nil - # array # => StaticArray[2, 2, 2] - # ``` - @[Deprecated("Use `#fill(value : T)` instead")] - def []=(value : T) - size.times do |i| - to_unsafe[i] = value - end - end - # Modifies `self` by randomizing the order of elements in the array # using the given *random* number generator. Returns `self`. # diff --git a/src/string.cr b/src/string.cr index d0e5ba852b32..03b57c8c6e70 100644 --- a/src/string.cr +++ b/src/string.cr @@ -4079,19 +4079,6 @@ class String just len, char, -1 end - # Adds spaces to right of the string until it is at least size of *len*, - # and then appends the result to the given IO. - # - # ``` - # io = IO::Memory.new - # "Purple".ljust(8, io) - # io.to_s # => "Purple " - # ``` - @[Deprecated("Use `#ljust(io :IO, len : Int, char : Char = ' ')` instead")] - def ljust(len : Int, io : IO) : Nil - ljust(io, len) - end - # Adds instances of *char* to right of the string until it is at least size of *len*, # and then appends the result to the given IO. # @@ -4105,19 +4092,6 @@ class String (len - size).times { io << char } end - # Adds instances of *char* to right of the string until it is at least size of *len*, - # and then appends the result to the given IO. - # - # ``` - # io = IO::Memory.new - # "Purple".ljust(8, '-', io) - # io.to_s # => "Purple--" - # ``` - @[Deprecated("Use `#ljust(io :IO, len : Int, char : Char = ' ')` instead")] - def ljust(len : Int, char : Char, io : IO) : Nil - ljust(io, len, char) - end - # Adds instances of *char* to left of the string until it is at least size of *len*. # # ``` @@ -4129,19 +4103,6 @@ class String just len, char, 1 end - # Adds spaces to left of the string until it is at least size of *len*, - # and then appends the result to the given IO. - # - # ``` - # io = IO::Memory.new - # "Purple".rjust(8, io) - # io.to_s # => " Purple" - # ``` - @[Deprecated("Use `#rjust(io :IO, len : Int, char : Char = ' ')` instead")] - def rjust(len : Int, io : IO) : Nil - rjust(io, len) - end - # Adds instances of *char* to left of the string until it is at least size of *len*, # and then appends the result to the given IO. # @@ -4155,19 +4116,6 @@ class String io << self end - # Adds instances of *char* to left of the string until it is at least size of *len*, - # and then appends the result to the given IO. - # - # ``` - # io = IO::Memory.new - # "Purple".rjust(8, '-', io) - # io.to_s # => "--Purple" - # ``` - @[Deprecated("Use `#rjust(io :IO, len : Int, char : Char = ' ')` instead")] - def rjust(len : Int, char : Char, io : IO) : Nil - rjust(io, len, char) - end - # Adds instances of *char* to left and right of the string until it is at least size of *len*. # # ``` @@ -4180,19 +4128,6 @@ class String just len, char, 0 end - # Adds spaces to left and right of the string until it is at least size of *len*, - # then appends the result to the given IO. - # - # ``` - # io = IO::Memory.new - # "Purple".center(9, io) - # io.to_s # => " Purple " - # ``` - @[Deprecated("Use `#center(io :IO, len : Int, char : Char = ' ')` instead")] - def center(len : Int, io : IO) : Nil - center(io, len) - end - # Adds instances of *char* to left and right of the string until it is at least size of *len*, # then appends the result to the given IO. # @@ -4217,19 +4152,6 @@ class String right_padding.times { io << char } end - # Adds instances of *char* to left and right of the string until it is at least size of *len*, - # then appends the result to the given IO. - # - # ``` - # io = IO::Memory.new - # "Purple".center(9, '-', io) - # io.to_s # => "-Purple--" - # ``` - @[Deprecated("Use `#center(io :IO, len : Int, char : Char = ' ')` instead")] - def center(len : Int, char : Char, io : IO) : Nil - center(io, len, char) - end - private def just(len, char, justify) return self if size >= len diff --git a/src/time.cr b/src/time.cr index 6e7532c52822..b36d8a4cef1e 100644 --- a/src/time.cr +++ b/src/time.cr @@ -1091,14 +1091,6 @@ struct Time Format.new(format).format(self, io) end - # Formats this `Time` according to the pattern in *format* to the given *io*. - # - # See `Time::Format` for details. - @[Deprecated("Use `#to_s(io : IO, format : String)` instead")] - def to_s(format : String, io : IO) : Nil - to_s(io, format) - end - # Format this time using the format specified by [RFC 3339](https://tools.ietf.org/html/rfc3339) ([ISO 8601](http://xml.coverpages.org/ISO-FDIS-8601.pdf) profile). # # ``` diff --git a/src/time/span.cr b/src/time/span.cr index f9321fd0c527..ec50e9a990f7 100644 --- a/src/time/span.cr +++ b/src/time/span.cr @@ -227,12 +227,6 @@ struct Time::Span @seconds end - # Alias of `abs`. - @[Deprecated("Use `#abs` instead.")] - def duration : Time::Span - abs - end - # Returns the absolute (non-negative) amount of time this `Time::Span` # represents by removing the sign. def abs : Time::Span diff --git a/src/uri.cr b/src/uri.cr index 1d675c03f50c..9e079f0017e2 100644 --- a/src/uri.cr +++ b/src/uri.cr @@ -232,12 +232,6 @@ class URI end end - # :ditto: - @[Deprecated("Use `#request_target` instead.")] - def full_path : String - request_target - end - # Returns `true` if URI has a *scheme* specified. def absolute? : Bool @scheme ? true : false diff --git a/src/yaml/builder.cr b/src/yaml/builder.cr index 29f4cf954c84..77924f184809 100644 --- a/src/yaml/builder.cr +++ b/src/yaml/builder.cr @@ -56,14 +56,6 @@ class YAML::Builder io.flush end - # :ditto: - @[Deprecated("Use .build instead")] - def self.new(io : IO, & : self ->) : Nil - build(io) do |builder| - yield builder - end - end - # Starts a YAML stream. def start_stream emit stream_start, LibYAML::Encoding::UTF8 From 67e282943d6d25d2f91ee0c092d3fffed098f934 Mon Sep 17 00:00:00 2001 From: Julien Reichardt Date: Tue, 9 Mar 2021 22:00:51 +0100 Subject: [PATCH 05/33] Add GitHub issue templates (#8934) * Add GitHub issue templates * Fix Stack Overflow wording Co-Authored-By: Sijawusz Pur Rahnama * Fix typo Co-Authored-By: Sijawusz Pur Rahnama * Better phrasing Co-Authored-By: Sijawusz Pur Rahnama * Change discussion to question * Improve template chooser sentences * Remove duplication * Put back status:discussion Co-authored-by: Sijawusz Pur Rahnama --- .../ISSUE_TEMPLATE/bug-report.md | 12 +++++++----- .github/ISSUE_TEMPLATE/config.yml | 8 ++++++++ .github/ISSUE_TEMPLATE/discussion.md | 12 ++++++++++++ .github/ISSUE_TEMPLATE/feature-request.md | 12 ++++++++++++ 4 files changed, 39 insertions(+), 5 deletions(-) rename ISSUE_TEMPLATE.md => .github/ISSUE_TEMPLATE/bug-report.md (60%) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/discussion.md create mode 100644 .github/ISSUE_TEMPLATE/feature-request.md diff --git a/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE/bug-report.md similarity index 60% rename from ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE/bug-report.md index a7ac9727bf85..b1dfe12a53e7 100644 --- a/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -1,12 +1,14 @@ -# Submitting questions +--- +name: "\U0001F41B Bug Report" +about: I want to report a bug. +labels: kind:bug +--- -If you have a question like "how do I do X?", this is not the right place for asking it. Please ask on the [Crystal Forum](https://forum.crystal-lang.org), on our combined [Gitter](https://gitter.im/crystal-lang/crystal)/[IRC](http://webchat.freenode.net/?channels=#crystal-lang) or on [StackOverflow](http://stackoverflow.com/questions/tagged/crystal-lang). - -# Submitting bugs +## Bug Report Make sure to review these points before submitting issues - thank you! -- Make sure a similar issue doesn't exist yet: use the search box +- Make sure a similar issue does not exist yet: use the search box, search engine and look at [Stack Overflow](https://stackoverflow.com/questions/tagged/crystal-lang). - **Include reproducible code**: we should be able to paste it into an editor, compile and run it and get the same error as you. Otherwise it's impossible for us to reproduce the bug. - Don't **only** use `play.crystal-lang.org` or `carc.in`: code might be lost and the issue will remain incomplete. Write code in the issue itself. - Reduce code, if possible, to the minimum size that reproduces the bug. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000000..0ce7355958ee --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: "\U00002753 Crystal Community Forum" + url: https://forum.crystal-lang.org/c/help-support + about: Questions about installing, using Crystal and any related issues. + - name: "\U0001F4AC Crystal Community Chat" + url: https://gitter.im/crystal-lang/crystal + about: "Get in touch with the community, ask for help and talk about Crystal. (IRC: #crystal-lang on freenode)" diff --git a/.github/ISSUE_TEMPLATE/discussion.md b/.github/ISSUE_TEMPLATE/discussion.md new file mode 100644 index 000000000000..344ec6995d75 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/discussion.md @@ -0,0 +1,12 @@ +--- +name: "\U0001F914 Language Improvement Discussion" +about: I want to start a new discussion about improving the language. +labels: status:discussion +--- + +## Discussion + +- What aspect of the language would you like to see improved? +- What are the reasons? +- Include practical examples to illustrate your points. +- Optionally add one (or more) proposals to improve the current situation. diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 000000000000..756c4cfd2816 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,12 @@ +--- +name: "\U0001F680 Feature Request" +about: I want to propose a new language feature. +labels: kind:feature +--- + +## Feature Request + +- Is your feature request related to a problem? Please describe clearly and concisely what is it. +- Describe the feature you would like, optionally illustrated by examples, and how it will solve the above problem. +- Describe considered alternative solutions, and the reasons why you have not proposed them as a solution here. +- Does it break backward compatibility, if yes then what's the migration path? From b02581fc0b367ac0027763fc13bd1967df89657d Mon Sep 17 00:00:00 2001 From: Kenichi Kamiya Date: Wed, 10 Mar 2021 06:47:43 +0900 Subject: [PATCH 06/33] Fix doc Set#each returs `nil` instead of `self` (#10477) --- src/set.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/set.cr b/src/set.cr index b44fadaaf9fe..17bad6dd0712 100644 --- a/src/set.cr +++ b/src/set.cr @@ -185,8 +185,8 @@ struct Set(T) @hash.empty? end - # Yields each element of the set, and returns `self`. - def each + # Yields each element of the set, and returns `nil`. + def each : Nil @hash.each_key do |key| yield key end From 3bc7a9a66358b91d762cbe83180308a681535773 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Wed, 10 Mar 2021 06:05:09 +0800 Subject: [PATCH 07/33] Exclude abstract defs from "no overload matches" errors (#10483) --- spec/compiler/semantic/abstract_def_spec.cr | 7 ++++++- src/compiler/crystal/semantic/call_error.cr | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/spec/compiler/semantic/abstract_def_spec.cr b/spec/compiler/semantic/abstract_def_spec.cr index bb9e9907e994..4917e2c36a9c 100644 --- a/spec/compiler/semantic/abstract_def_spec.cr +++ b/spec/compiler/semantic/abstract_def_spec.cr @@ -94,7 +94,12 @@ describe "Semantic: abstract def" do Bar.new.foo(1 || 'a') ), - "no overload matches" + <<-MSG + Overloads are: + - Bar#foo(x : Int32) + Couldn't find overloads for these types: + - Bar#foo(x : Char) + MSG end it "errors if using abstract def on non-abstract class" do diff --git a/src/compiler/crystal/semantic/call_error.cr b/src/compiler/crystal/semantic/call_error.cr index eda3c4f47bbd..d03318a64a7f 100644 --- a/src/compiler/crystal/semantic/call_error.cr +++ b/src/compiler/crystal/semantic/call_error.cr @@ -425,6 +425,7 @@ class Crystal::Call def append_matches(defs, arg_types, str, *, matched_def = nil, argument_name = nil) defs.each do |a_def| + next if a_def.abstract? str << "\n - " append_def_full_name a_def.owner, a_def, arg_types, str if defs.size > 1 && a_def.same?(matched_def) From a88563ae1ca5d336dab1e9df989e4b03ec2a9a97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Wed, 10 Mar 2021 08:36:23 +0100 Subject: [PATCH 08/33] Remove implicit en-/decoding for cookie values (#10485) --- spec/std/http/cookie_spec.cr | 106 ++++++++++++++++++++++++++++++----- src/http/cookie.cr | 55 ++++++++++++++++-- 2 files changed, 141 insertions(+), 20 deletions(-) diff --git a/spec/std/http/cookie_spec.cr b/spec/std/http/cookie_spec.cr index fb6c494106af..05fb85079ae3 100644 --- a/spec/std/http/cookie_spec.cr +++ b/spec/std/http/cookie_spec.cr @@ -15,6 +15,93 @@ private def parse_set_cookie(header) end module HTTP + describe Cookie do + describe ".new" do + it "raises on invalid name" do + expect_raises IO::Error, "Invalid cookie name" do + HTTP::Cookie.new("", "") + end + expect_raises IO::Error, "Invalid cookie name" do + HTTP::Cookie.new("\t", "") + end + # more extensive specs on #name= + end + + it "raises on invalid value" do + expect_raises IO::Error, "Invalid cookie value" do + HTTP::Cookie.new("x", %(foo\rbar)) + end + # more extensive specs on #value= + end + end + + describe "#name=" do + it "raises on invalid name" do + cookie = HTTP::Cookie.new("x", "") + expect_raises IO::Error, "Invalid cookie name" do + cookie.name = "" + end + expect_raises IO::Error, "Invalid cookie name" do + cookie.name = "\t" + end + expect_raises IO::Error, "Invalid cookie name" do + cookie.name = "\r" + end + expect_raises IO::Error, "Invalid cookie name" do + cookie.name = "a\nb" + end + expect_raises IO::Error, "Invalid cookie name" do + cookie.name = "a\rb" + end + end + end + + describe "#value=" do + it "raises on invalid value" do + cookie = HTTP::Cookie.new("x", "") + expect_raises IO::Error, "Invalid cookie value" do + cookie.value = %(foo\rbar) + end + expect_raises IO::Error, "Invalid cookie value" do + cookie.value = %(foo"bar) + end + expect_raises IO::Error, "Invalid cookie value" do + cookie.value = "foo;bar" + end + expect_raises IO::Error, "Invalid cookie value" do + cookie.value = "foo\\bar" + end + expect_raises IO::Error, "Invalid cookie value" do + cookie.value = "foo\\bar" + end + expect_raises IO::Error, "Invalid cookie value" do + cookie.value = "foo bar" + end + expect_raises IO::Error, "Invalid cookie value" do + cookie.value = "foo,bar" + end + end + end + + describe "#to_set_cookie_header" do + it { HTTP::Cookie.new("x", "v$1").to_set_cookie_header.should eq "x=v$1; path=/" } + + it { HTTP::Cookie.new("x", "seven", domain: "127.0.0.1").to_set_cookie_header.should eq "x=seven; domain=127.0.0.1; path=/" } + + it { HTTP::Cookie.new("x", "expiring", expires: Time.unix(1257894000)).to_set_cookie_header.should eq "x=expiring; path=/; expires=Tue, 10 Nov 2009 23:00:00 GMT" } + it { HTTP::Cookie.new("x", "expiring-1601", expires: Time.utc(1601, 1, 1, 1, 1, 1, nanosecond: 1)).to_set_cookie_header.should eq "x=expiring-1601; path=/; expires=Mon, 01 Jan 1601 01:01:01 GMT" } + + it "samesite" do + HTTP::Cookie.new("x", "samesite-default", samesite: nil).to_set_cookie_header.should eq "x=samesite-default; path=/" + HTTP::Cookie.new("x", "samesite-lax", samesite: :lax).to_set_cookie_header.should eq "x=samesite-lax; path=/; SameSite=Lax" + HTTP::Cookie.new("x", "samesite-strict", samesite: :strict).to_set_cookie_header.should eq "x=samesite-strict; path=/; SameSite=Strict" + HTTP::Cookie.new("x", "samesite-none", samesite: :none).to_set_cookie_header.should eq "x=samesite-none; path=/; SameSite=None" + end + + it { HTTP::Cookie.new("empty-value", "").to_set_cookie_header.should eq "empty-value=; path=/" } + end + end + describe Cookie::Parser do describe "parse_cookies" do it "parses key=value" do @@ -41,17 +128,17 @@ module HTTP cookie = parse_first_cookie("key=key=value") cookie.name.should eq("key") cookie.value.should eq("key=value") - cookie.to_set_cookie_header.should eq("key=key%3Dvalue; path=/") + cookie.to_set_cookie_header.should eq("key=key=value; path=/") end it "parses key=key%3Dvalue" do cookie = parse_first_cookie("key=key%3Dvalue") cookie.name.should eq("key") - cookie.value.should eq("key=value") + cookie.value.should eq("key%3Dvalue") cookie.to_set_cookie_header.should eq("key=key%3Dvalue; path=/") end - it "only decodes values" do + it "parses special character in name" do cookie = parse_first_cookie("key%3Dvalue=value") cookie.name.should eq("key%3Dvalue") cookie.value.should eq("value") @@ -248,15 +335,6 @@ module HTTP cookies.has_key?("a").should be_true end - it "allows adding and retrieving cookies with reserved chars" do - cookies = Cookies.new - cookies << Cookie.new("a[0]", "b+c%20") - cookies["d"] = "e+f" - - cookies["a[0]"].value.should eq "b+c%20" - cookies["d"].value.should eq "e+f" - end - it "allows retrieving the size of the cookies collection" do cookies = Cookies.new cookies.size.should eq 0 @@ -306,7 +384,7 @@ module HTTP cookies = Cookies.new cookies << Cookie.new("a", "b+c") cookies.add_request_headers(headers) - headers["Cookie"].should eq "a=b%2Bc" + headers["Cookie"].should eq "a=b+c" end it "merges multiple cookies into one Cookie header" do @@ -371,7 +449,7 @@ module HTTP cookies = Cookies.new cookies << Cookie.new("a", "b+c") cookies.add_response_headers(headers) - headers.get("Set-Cookie").includes?("a=b%2Bc; path=/").should be_true + headers.get("Set-Cookie").includes?("a=b+c; path=/").should be_true end describe "when no cookies are set" do diff --git a/src/http/cookie.cr b/src/http/cookie.cr index 65879c6cbdb8..a79b9c3ea027 100644 --- a/src/http/cookie.cr +++ b/src/http/cookie.cr @@ -15,8 +15,8 @@ module HTTP Lax end - property name : String - property value : String + getter name : String + getter value : String property path : String property expires : Time? property domain : String? @@ -27,14 +27,57 @@ module HTTP def_equals_and_hash name, value, path, expires, domain, secure, http_only - def initialize(@name : String, value : String, @path : String = "/", + # Creates a new `Cookie` instance. + # + # Raises `IO::Error` if *name* or *value* are invalid as per [RFC 6265 §4.1.1](https://tools.ietf.org/html/rfc6265#section-4.1.1). + def initialize(name : String, value : String, @path : String = "/", @expires : Time? = nil, @domain : String? = nil, @secure : Bool = false, @http_only : Bool = false, @samesite : SameSite? = nil, @extension : String? = nil) + validate_name(name) @name = name + validate_value(value) @value = value end + # Sets the name of this cookie. + # + # Raises `IO::Error` if the value is invalid as per [RFC 6265 §4.1.1](https://tools.ietf.org/html/rfc6265#section-4.1.1). + def name=(name : String) + validate_name(name) + @name = name + end + + private def validate_name(name) + raise IO::Error.new("Invalid cookie name") if name.empty? + name.each_byte do |byte| + # valid characters for cookie-name per https://tools.ietf.org/html/rfc6265#section-4.1.1 + # and https://tools.ietf.org/html/rfc2616#section-2.2 + # "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUWVXYZ^_`abcdefghijklmnopqrstuvwxyz|~" + unless (0x21...0x7f).includes?(byte) && byte != 0x22 && byte != 0x28 && byte != 0x29 && byte != 0x2c && byte != 0x2f && !(0x3a..0x40).includes?(byte) && !(0x5b..0x5d).includes?(byte) && byte != 0x7b && byte != 0x7d + raise IO::Error.new("Invalid cookie name") + end + end + end + + # Sets the value of this cookie. + # + # Raises `IO::Error` if the value is invalid as per [RFC 6265 §4.1.1](https://tools.ietf.org/html/rfc6265#section-4.1.1). + def value=(value : String) + validate_value(value) + @value = value + end + + private def validate_value(value) + value.each_byte do |byte| + # valid characters for cookie-value per https://tools.ietf.org/html/rfc6265#section-4.1.1 + # all printable ASCII characters except ' ', ',', '"', ';' and '\\' + unless (0x21...0x7f).includes?(byte) && byte != 0x22 && byte != 0x2c && byte != 0x3b && byte != 0x5c + raise IO::Error.new("Invalid cookie value") + end + end + end + def to_set_cookie_header path = @path expires = @expires @@ -61,7 +104,7 @@ module HTTP def to_cookie_header(io) io << @name io << '=' - URI.encode_www_form(value, io) + io << @value end def expired? @@ -109,7 +152,7 @@ module HTTP def parse_cookies(header) header.scan(CookieString).each do |pair| - yield Cookie.new(pair["name"], URI.decode_www_form(pair["value"])) + yield Cookie.new(pair["name"], pair["value"]) end end @@ -130,7 +173,7 @@ module HTTP end Cookie.new( - URI.decode_www_form(match["name"]), URI.decode_www_form(match["value"]), + match["name"], match["value"], path: match["path"]? || "/", expires: expires, domain: match["domain"]?, From 7d0f64bad4ce93e9eb86952e4c1dd7f2bba34c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Wed, 10 Mar 2021 08:41:46 +0100 Subject: [PATCH 09/33] Fix HTTP::Cookies.from_headers separate for server/client (#10486) --- spec/std/http/cookie_spec.cr | 22 ++++++++++++++++++++++ src/http/client/response.cr | 2 +- src/http/cookie.cr | 27 +++++++++++++++++++++++++++ src/http/request.cr | 2 +- 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/spec/std/http/cookie_spec.cr b/spec/std/http/cookie_spec.cr index 05fb85079ae3..09d6511f15cc 100644 --- a/spec/std/http/cookie_spec.cr +++ b/spec/std/http/cookie_spec.cr @@ -321,6 +321,28 @@ module HTTP end describe Cookies do + describe ".from_client_headers" do + it "parses Cookie header" do + cookies = Cookies.from_client_headers Headers{"Cookie" => "a=b"} + cookies.to_h.should eq({"a" => Cookie.new("a", "b")}) + end + it "does not accept Set-Cookie header" do + cookies = Cookies.from_client_headers Headers{"Cookie" => "a=b", "Set-Cookie" => "x=y"} + cookies.to_h.should eq({"a" => Cookie.new("a", "b")}) + end + end + + describe ".from_server_headers" do + it "parses Set-Cookie header" do + cookies = Cookies.from_server_headers Headers{"Set-Cookie" => "a=b; path=/foo"} + cookies.to_h.should eq({"a" => Cookie.new("a", "b", path: "/foo")}) + end + it "does not accept Cookie header" do + cookies = Cookies.from_server_headers Headers{"Set-Cookie" => "a=b", "Cookie" => "x=y"} + cookies.to_h.should eq({"a" => Cookie.new("a", "b")}) + end + end + it "allows adding cookies and retrieving" do cookies = Cookies.new cookies << Cookie.new("a", "b") diff --git a/src/http/client/response.cr b/src/http/client/response.cr index 0218bf9a2644..3b81b523eb84 100644 --- a/src/http/client/response.cr +++ b/src/http/client/response.cr @@ -41,7 +41,7 @@ class HTTP::Client::Response # Returns a convenience wrapper around querying and setting cookie related # headers, see `HTTP::Cookies`. def cookies - @cookies ||= Cookies.from_headers(headers) + @cookies ||= Cookies.from_server_headers(headers) end def keep_alive? diff --git a/src/http/cookie.cr b/src/http/cookie.cr index a79b9c3ea027..78dd723d599b 100644 --- a/src/http/cookie.cr +++ b/src/http/cookie.cr @@ -202,19 +202,46 @@ module HTTP # headers in the given `HTTP::Headers`. # # See `HTTP::Request#cookies` and `HTTP::Client::Response#cookies`. + @[Deprecated("Use `.from_client_headers` or `.from_server_headers` instead.")] def self.from_headers(headers) : self new.tap { |cookies| cookies.fill_from_headers(headers) } end # Filling cookies by parsing the `Cookie` and `Set-Cookie` # headers in the given `HTTP::Headers`. + @[Deprecated("Use `#fill_from_client_headers` or `#fill_from_server_headers` instead.")] def fill_from_headers(headers) + fill_from_client_headers(headers) + fill_from_server_headers(headers) + self + end + + # Creates a new instance by parsing the `Cookie` headers in the given `HTTP::Headers`. + # + # See `HTTP::Client::Response#cookies`. + def self.from_client_headers(headers) : self + new.tap { |cookies| cookies.fill_from_client_headers(headers) } + end + + # Filling cookies by parsing the `Cookie` headers in the given `HTTP::Headers`. + def fill_from_client_headers(headers) if values = headers.get?("Cookie") values.each do |header| Cookie::Parser.parse_cookies(header) { |cookie| self << cookie } end end + self + end + + # Creates a new instance by parsing the `Set-Cookie` headers in the given `HTTP::Headers`. + # + # See `HTTP::Request#cookies`. + def self.from_server_headers(headers) : self + new.tap { |cookies| cookies.fill_from_server_headers(headers) } + end + # Filling cookies by parsing the `Set-Cookie` headers in the given `HTTP::Headers`. + def fill_from_server_headers(headers) if values = headers.get?("Set-Cookie") values.each do |header| Cookie::Parser.parse_set_cookie(header).try { |cookie| self << cookie } diff --git a/src/http/request.cr b/src/http/request.cr index b2f2976229c4..c7547bd4ad3c 100644 --- a/src/http/request.cr +++ b/src/http/request.cr @@ -58,7 +58,7 @@ class HTTP::Request # Returns a convenience wrapper around querying and setting cookie related # headers, see `HTTP::Cookies`. def cookies - @cookies ||= Cookies.from_headers(headers) + @cookies ||= Cookies.from_client_headers(headers) end # Returns a convenience wrapper around querying and setting query params, From 9a3d0c4b1ac7e9a9392b4d592598a6dbdd7388ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Wed, 10 Mar 2021 09:19:11 +0100 Subject: [PATCH 10/33] Minor performance improvements to `HTTP::Cookies` (#10488) --- src/http/cookie.cr | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/http/cookie.cr b/src/http/cookie.cr index 78dd723d599b..128ca3ae8c02 100644 --- a/src/http/cookie.cr +++ b/src/http/cookie.cr @@ -342,8 +342,8 @@ module HTTP end # Yields each `HTTP::Cookie` in the collection. - def each(&block : Cookie ->) - @cookies.values.each do |cookie| + def each(& : Cookie ->) + @cookies.each_value do |cookie| yield cookie end end @@ -367,8 +367,15 @@ module HTTP # given `HTTP::Headers` instance and returns it. Removes any existing # `Cookie` headers in it. def add_request_headers(headers) - headers.delete("Cookie") - headers.add("Cookie", map(&.to_cookie_header).join("; ")) unless empty? + if empty? + headers.delete("Cookie") + else + capacity = sum { |cookie| cookie.name.bytesize + cookie.value.bytesize + 1 } + capacity += (size - 1) * 2 + headers["Cookie"] = String.build(capacity) do |io| + join(io, "; ", &.to_cookie_header(io)) + end + end headers end From a02e746e67db501e5ee24cd0300c2c43ae71761e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Wed, 10 Mar 2021 14:24:39 +0100 Subject: [PATCH 11/33] Fix Cookie#== to take all ivars into account (#10487) --- spec/std/http/cookie_spec.cr | 14 ++++++++++++++ src/http/cookie.cr | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/spec/std/http/cookie_spec.cr b/spec/std/http/cookie_spec.cr index 09d6511f15cc..9ac457ed6ecd 100644 --- a/spec/std/http/cookie_spec.cr +++ b/spec/std/http/cookie_spec.cr @@ -16,6 +16,20 @@ end module HTTP describe Cookie do + it "#==" do + cookie = Cookie.new("a", "b", path: "/path", expires: Time.utc, domain: "domain", secure: true, http_only: true, samesite: :strict, extension: "foo=bar") + cookie.should eq(cookie.dup) + cookie.should_not eq(cookie.dup.tap { |c| c.name = "c" }) + cookie.should_not eq(cookie.dup.tap { |c| c.value = "c" }) + cookie.should_not eq(cookie.dup.tap { |c| c.path = "/c" }) + cookie.should_not eq(cookie.dup.tap { |c| c.domain = "c" }) + cookie.should_not eq(cookie.dup.tap { |c| c.expires = Time.utc(2021, 1, 1) }) + cookie.should_not eq(cookie.dup.tap { |c| c.secure = false }) + cookie.should_not eq(cookie.dup.tap { |c| c.http_only = false }) + cookie.should_not eq(cookie.dup.tap { |c| c.samesite = :lax }) + cookie.should_not eq(cookie.dup.tap { |c| c.extension = nil }) + end + describe ".new" do it "raises on invalid name" do expect_raises IO::Error, "Invalid cookie name" do diff --git a/src/http/cookie.cr b/src/http/cookie.cr index 128ca3ae8c02..d4f6de7c365e 100644 --- a/src/http/cookie.cr +++ b/src/http/cookie.cr @@ -25,7 +25,7 @@ module HTTP property samesite : SameSite? property extension : String? - def_equals_and_hash name, value, path, expires, domain, secure, http_only + def_equals_and_hash name, value, path, expires, domain, secure, http_only, samesite, extension # Creates a new `Cookie` instance. # From 2588dc59f87614eac54887909e4c4228daca96f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Wed, 10 Mar 2021 16:42:05 +0100 Subject: [PATCH 12/33] Fix using roundeven intrinsic on darwin (#10479) * [CI] Re-enable test_darwin on circle-ci * Fix using roundeven intrinsic on darwin For some reason the `roundeven` intrinsic is not available in LLVM 11 on macos, so we fall back to `rint`. * Remove roundeven for now --- .circleci/config.yml | 3 +++ src/float.cr | 20 ++++++-------------- src/math/libm.cr | 4 ---- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2b7706a4fb35..4c457723915e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -510,6 +510,9 @@ jobs: workflows: version: 2 + test_all_platforms: + jobs: + - test_darwin tagged_release: jobs: - test_linux: diff --git a/src/float.cr b/src/float.cr index cee021be938b..6f427f2e45b9 100644 --- a/src/float.cr +++ b/src/float.cr @@ -154,13 +154,9 @@ struct Float32 # Rounds towards the nearest integer. If both neighboring integers are equidistant, # rounds towards the even neighbor (Banker's rounding). def round_even : self - # LLVM 11 introduced llvm.roundeven.* intrinsics which may replace rint in - # the future. - {% if compare_versions(Crystal::LLVM_VERSION, "11.0.0") >= 0 %} - LibM.roundeven_f32(self) - {% else %} - LibM.rint_f32(self) - {% end %} + # TODO: LLVM 11 introduced llvm.roundeven.* intrinsics which may replace + # rint in the future. + LibM.rint_f32(self) end # Rounds towards the nearest integer. If both neighboring integers are equidistant, @@ -255,13 +251,9 @@ struct Float64 # Rounds towards the nearest integer. If both neighboring integers are equidistant, # rounds towards the even neighbor (Banker's rounding). def round_even : self - # LLVM 11 introduced llvm.roundeven.* intrinsics which may replace rint in - # the future. - {% if compare_versions(Crystal::LLVM_VERSION, "11.0.0") >= 0 %} - LibM.roundeven_f64(self) - {% else %} - LibM.rint_f64(self) - {% end %} + # TODO: LLVM 11 introduced llvm.roundeven.* intrinsics which may replace + # rint in the future. + LibM.rint_f64(self) end # Rounds towards the nearest integer. If both neighboring integers are equidistant, diff --git a/src/math/libm.cr b/src/math/libm.cr index c71bd2803c26..1840108f8d42 100644 --- a/src/math/libm.cr +++ b/src/math/libm.cr @@ -34,10 +34,6 @@ lib LibM {% end %} fun round_f32 = "llvm.round.f32"(value : Float32) : Float32 fun round_f64 = "llvm.round.f64"(value : Float64) : Float64 - {% if compare_versions(Crystal::LLVM_VERSION, "11.0.0") >= 0 %} - fun roundeven_f32 = "llvm.roundeven.f32"(value : Float32) : Float32 - fun roundeven_f64 = "llvm.roundeven.f64"(value : Float64) : Float64 - {% end %} fun rint_f32 = "llvm.rint.f32"(value : Float32) : Float32 fun rint_f64 = "llvm.rint.f64"(value : Float64) : Float64 fun sin_f32 = "llvm.sin.f32"(value : Float32) : Float32 From 7aa9e98a195a2da63f703be36e5694074a1179a9 Mon Sep 17 00:00:00 2001 From: "Brian J. Cardiff" Date: Wed, 10 Mar 2021 16:20:14 -0300 Subject: [PATCH 13/33] Bump distribution-scripts (#10494) --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4c457723915e..96765715089a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -144,7 +144,7 @@ jobs: - run: | git clone https://github.com/crystal-lang/distribution-scripts.git ~/distribution-scripts cd ~/distribution-scripts - git checkout 80c83b203e1ee2fd54e41398160a67c6a06d34e1 + git checkout 4caf7cd337087e2ae313b8c7e00be247d34482ad # persist relevant information for build process - run: | cd ~/distribution-scripts From 574a6e1a172b506d61d04e9e8b9cc2571de262e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Mon, 15 Mar 2021 15:39:58 +0100 Subject: [PATCH 14/33] [CI] Re-enable test_darwin on circle-ci (#10476) * [CI] Re-enable test_darwin on circle-ci * Fix brew update --- bin/ci | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bin/ci b/bin/ci index af0324bae40a..bdb4d53342d7 100755 --- a/bin/ci +++ b/bin/ci @@ -135,6 +135,17 @@ prepare_build() { on_osx curl -L https://github.com/crystal-lang/crystal/releases/download/0.36.1/crystal-0.36.1-1-darwin-x86_64.tar.gz -o ~/crystal.tar.gz on_osx 'pushd ~;gunzip -c ~/crystal.tar.gz | tar xopf -;mv crystal-0.36.1-1 crystal;popd' + + # These commands may take a few minutes to run due to the large size of the repositories. + # This restriction has been made on GitHub's request because updating shallow + # clones is an extremely expensive operation due to the tree layout and + # traffic of Homebrew/homebrew-core and Homebrew/homebrew-cask. We don't do + # this for you automatically to avoid repeatedly performing an expensive + # unshallow operation in CI systems (which should instead be fixed to not use + # shallow clones). Sorry for the inconvenience! + on_osx git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core fetch --unshallow + on_osx git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-cask fetch --unshallow + on_osx brew update --preinstall on_osx brew bundle --no-lock From 1b704d9dde3d11b8106054517844655ddf61273c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Mon, 15 Mar 2021 15:40:30 +0100 Subject: [PATCH 15/33] Change default rounding mode to TIES_EVEN (#10508) --- spec/std/benchmark_spec.cr | 2 +- spec/std/complex_spec.cr | 3 ++- spec/std/float_spec.cr | 3 ++- spec/std/humanize_spec.cr | 5 +++-- spec/std/number_spec.cr | 12 ++++++------ src/number.cr | 14 +++++++------- 6 files changed, 21 insertions(+), 18 deletions(-) diff --git a/spec/std/benchmark_spec.cr b/spec/std/benchmark_spec.cr index 7b4ded84b296..1bd3738c0f3b 100644 --- a/spec/std/benchmark_spec.cr +++ b/spec/std/benchmark_spec.cr @@ -108,7 +108,7 @@ describe Benchmark::IPS::Entry, "#human_iteration_time" do it { h_ips(0.000_001_234_567).should eq(" 1.23µs") } it { h_ips(0.000_000_123_456).should eq("123.46ns") } - it { h_ips(0.000_000_012_345).should eq(" 12.35ns") } + it { h_ips(0.000_000_012_345).should eq(" 12.34ns") } it { h_ips(0.000_000_001_234).should eq(" 1.23ns") } it { h_ips(0.000_000_000_123).should eq(" 0.12ns") } end diff --git a/spec/std/complex_spec.cr b/spec/std/complex_spec.cr index 8f6b41c7cc51..8056bf7cb340 100644 --- a/spec/std/complex_spec.cr +++ b/spec/std/complex_spec.cr @@ -178,7 +178,8 @@ describe "Complex" do end it "rounds" do - (Complex.new(1.125, 0.875).round(2)).should eq(Complex.new(1.13, 0.88)) + (Complex.new(1.125, 0.875).round(2)).should eq(Complex.new(1.12, 0.88)) + (Complex.new(1.135, 0.865).round(2)).should eq(Complex.new(1.14, 0.86)) (Complex.new(1.125, 0.875).round(digits: 1)).should eq(Complex.new(1.1, 0.9)) end diff --git a/spec/std/float_spec.cr b/spec/std/float_spec.cr index 342966ffab56..6bb67d5002ea 100644 --- a/spec/std/float_spec.cr +++ b/spec/std/float_spec.cr @@ -54,7 +54,8 @@ describe "Float" do end describe "round" do - it { 2.5.round.should eq(3) } + it { 2.5.round.should eq(2) } + it { 3.5.round.should eq(4) } it { 2.4.round.should eq(2) } end diff --git a/spec/std/humanize_spec.cr b/spec/std/humanize_spec.cr index ba5c4a69d1e9..5db505458274 100644 --- a/spec/std/humanize_spec.cr +++ b/spec/std/humanize_spec.cr @@ -72,7 +72,7 @@ describe Number do it { 0.000_001_234_567.humanize(precision: 2, significant: false).should eq("1.23µ") } it { 0.000_000_123_456.humanize(precision: 2, significant: false).should eq("123.46n") } - it { 0.000_000_012_345.humanize(precision: 2, significant: false).should eq("12.35n") } + it { 0.000_000_012_345.humanize(precision: 2, significant: false).should eq("12.34n") } it { 0.000_000_001_234.humanize(precision: 2, significant: false).should eq("1.23n") } it { 0.000_000_000_123.humanize(precision: 2, significant: false).should eq("123.00p") } @@ -102,7 +102,8 @@ describe Int do it { 1014.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq "0.99KB" } it { 1015.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq "1.0KB" } it { 1024.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq "1.0KB" } - it { 1025.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq "1.01KB" } + it { 1025.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq "1.0KB" } + it { 1026.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq "1.01KB" } it { 2048.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq "2.0KB" } it { 1536.humanize_bytes(format: Int::BinaryPrefixFormat::JEDEC).should eq("1.5KB") } diff --git a/spec/std/number_spec.cr b/spec/std/number_spec.cr index e74a45a67909..bda78452e6e5 100644 --- a/spec/std/number_spec.cr +++ b/spec/std/number_spec.cr @@ -130,7 +130,7 @@ describe "Number" do end it "handle medium amount of digits" do - 1.098765432109876543210987654321.round(15).should eq(1.098765432109877) + 1.098765432109876543210987654321.round(15).should eq(1.098765432109876) 1.098765432109876543210987654321.round(21).should eq(1.098765432109876543211) 6543210987654321.0.round(-15).should eq(7000000000000000.0) end @@ -210,20 +210,20 @@ describe "Number" do 2.5.round(:ties_away).should eq 3.0 end - it "default (=ties_away)" do - -2.5.round.should eq -3.0 + it "default (=ties_even)" do + -2.5.round.should eq -2.0 -1.5.round.should eq -2.0 -1.0.round.should eq -1.0 -0.9.round.should eq -1.0 - -0.5.round.should eq -1.0 + -0.5.round.should eq 0.0 -0.1.round.should eq 0.0 0.0.round.should eq 0.0 0.1.round.should eq 0.0 - 0.5.round.should eq 1.0 + 0.5.round.should eq 0.0 0.9.round.should eq 1.0 1.0.round.should eq 1.0 1.5.round.should eq 2.0 - 2.5.round.should eq 3.0 + 2.5.round.should eq 2.0 end end diff --git a/src/number.cr b/src/number.cr index d8e1ef8a9798..e181ed4ad58d 100644 --- a/src/number.cr +++ b/src/number.cr @@ -395,13 +395,13 @@ struct Number # (or before if negative), in base *base*. # # The rounding *mode* controls the direction of the rounding. The default is - # `RoundingMode::TIES_AWAY` which rounds to the nearest integer, with ties - # (fractional value of `0.5`) being rounded away from zero. + # `RoundingMode::TIES_EVEN` which rounds to the nearest integer, with ties + # (fractional value of `0.5`) being rounded to the even neighbor (Banker's rounding). # # ``` # -1763.116.round(2) # => -1763.12 # ``` - def round(digits : Number, base = 10, *, mode : RoundingMode = :ties_away) + def round(digits : Number, base = 10, *, mode : RoundingMode = :ties_even) if digits < 0 multiplier = base.to_f ** digits.abs shifted = self / multiplier @@ -444,10 +444,10 @@ struct Number # Rounds `self` to an integer value using rounding *mode*. # - # The rounding mode controls the direction of the rounding. The default is - # `RoundingMode::TIES_AWAY` which rounds to the nearest integer, with ties - # (fractional value of `0.5`) being rounded away from zero. - def round(mode : RoundingMode = :ties_away) : self + # The rounding *mode* controls the direction of the rounding. The default is + # `RoundingMode::TIES_EVEN` which rounds to the nearest integer, with ties + # (fractional value of `0.5`) being rounded to the even neighbor (Banker's rounding). + def round(mode : RoundingMode = :ties_even) : self case mode in .to_zero? trunc From aa91eb3807ed7e084144244ced4d6e8e3df41c82 Mon Sep 17 00:00:00 2001 From: maiha Date: Mon, 15 Mar 2021 23:40:56 +0900 Subject: [PATCH 16/33] examples: fix (2021-03) (#10505) * examples: fix (2021-03) * Update src/uri.cr Co-authored-by: Brian J. Cardiff * Update src/range.cr Co-authored-by: Sijawusz Pur Rahnama * revert to_json for self-contained example * run formatter * revert to_yaml for self-contained example Co-authored-by: Brian J. Cardiff Co-authored-by: Sijawusz Pur Rahnama --- src/hash.cr | 1 - src/http/client.cr | 2 +- src/json/from_json.cr | 9 ++++++--- src/json/pull_parser.cr | 10 +++++----- src/json/to_json.cr | 4 ++-- src/range.cr | 8 ++++---- src/string.cr | 4 ++-- src/uri.cr | 4 ++-- src/yaml/to_yaml.cr | 18 +++++++++--------- 9 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/hash.cr b/src/hash.cr index acf4a45d234d..788720e962b1 100644 --- a/src/hash.cr +++ b/src/hash.cr @@ -1522,7 +1522,6 @@ class Hash(K, V) # ``` # hash = {"hello" => "world", "foo" => nil} # hash.compact! # => {"hello" => "world"} - # hash.compact! # => nil # ``` def compact! reject! { |key, value| value.nil? } diff --git a/src/http/client.cr b/src/http/client.cr index a90a40d04c8a..b6b251664dc9 100644 --- a/src/http/client.cr +++ b/src/http/client.cr @@ -25,7 +25,7 @@ # ``` # require "http/client" # -# params = URI::Params.encode({"author" => "John Doe", "offset" => "20"}) # => author=John+Doe&offset=20 +# params = URI::Params.encode({"author" => "John Doe", "offset" => "20"}) # => "author=John+Doe&offset=20" # response = HTTP::Client.get URI.new("http", "www.example.com", query: params) # response.status_code # => 200 # ``` diff --git a/src/json/from_json.cr b/src/json/from_json.cr index 8482b57c643d..facbdc2d6370 100644 --- a/src/json/from_json.cr +++ b/src/json/from_json.cr @@ -285,11 +285,14 @@ end # @[JSON::Field(converter: Enum::ValueConverter(MyEnum))] # @[YAML::Field(converter: Enum::ValueConverter(MyEnum))] # property foo : MyEnum = MyEnum::ONE +# +# def initialize(@foo) +# end # end # -# foo = Foo.new -# foo.to_json # => %({"my_enum":1}) -# foo.to_yaml # => %(---\nmy_enum: 1\n) +# foo = Foo.new(MyEnum::ONE) +# foo.to_json # => %({"foo":1}) +# foo.to_yaml # => %(---\nfoo: 1\n) # ``` # # NOTE: Automatically assigned enum values are subject to change when the order diff --git a/src/json/pull_parser.cr b/src/json/pull_parser.cr index 7f33412256e9..a4591f98e602 100644 --- a/src/json/pull_parser.cr +++ b/src/json/pull_parser.cr @@ -230,14 +230,14 @@ class JSON::PullParser # If the value in unknown, it raises a `ParseException`. # # ``` - # pull = JSON::PullParser.new %([nil, true, 1, "foo", [1, "two"], {"foo": "bar"}]) + # pull = JSON::PullParser.new %([null, true, 1, "foo", [1, "two"], {"foo": "bar"}]) # pull.read_begin_array - # pull.read_raw # => "nil" + # pull.read_raw # => "null" # pull.read_raw # => "true" # pull.read_raw # => "1" - # pull.read_raw # => "foo" - # pull.read_raw # => "[1, \"two\"]" - # pull.read_raw # => "{\"foo\": \"bar\"}" + # pull.read_raw # => "\"foo\"" + # pull.read_raw # => "[1,\"two\"]" + # pull.read_raw # => "{\"foo\":\"bar\"}" # pull.read_end_array # ``` def read_raw diff --git a/src/json/to_json.cr b/src/json/to_json.cr index a6ea0ac04ae4..819a9b02a3f6 100644 --- a/src/json/to_json.cr +++ b/src/json/to_json.cr @@ -188,8 +188,8 @@ struct Enum # end # # Sides::LEFT.to_json # => %(["left"]) - # (Sides::LEFT | Sides::RIGHT).to_json # => %(["left", "right"]) - # Sides::All.to_json # => %(["left", "right"]) + # (Sides::LEFT | Sides::RIGHT).to_json # => %(["left","right"]) + # Sides::All.to_json # => %(["left","right"]) # Sides::None.to_json # => %([]) # ``` # diff --git a/src/range.cr b/src/range.cr index 86df380d8150..4644e8fb6593 100644 --- a/src/range.cr +++ b/src/range.cr @@ -213,10 +213,10 @@ struct Range(B, E) # (1..4).step(by: 2) do |x| # ary << x # end - # ary # => [1, 3] - # (1..4).step(by: 2).to_a # => [1, 3] - # (1..4).step(by: 1).to_a # => [1, 2, 3, 4] - # (1..4).step(by: 1, exclusive: true).to_a # => [1, 2, 3] + # ary # => [1, 3] + # (1..4).step(by: 2).to_a # => [1, 3] + # (1..4).step(by: 1).to_a # => [1, 2, 3, 4] + # (1...4).step(by: 1).to_a # => [1, 2, 3] # ``` # # The implementation is based on `B#step` method if available. The interface diff --git a/src/string.cr b/src/string.cr index 03b57c8c6e70..a8e30781cbd3 100644 --- a/src/string.cr +++ b/src/string.cr @@ -4108,7 +4108,7 @@ class String # # ``` # io = IO::Memory.new - # "Purple".rjust(8, '-', io) + # "Purple".rjust(io, 8, '-') # io.to_s # => "--Purple" # ``` def rjust(io : IO, len : Int, char : Char = ' ') : Nil @@ -4133,7 +4133,7 @@ class String # # ``` # io = IO::Memory.new - # "Purple".center(9, '-', io) + # "Purple".center(io, 9, '-') # io.to_s # => "-Purple--" # ``` def center(io : IO, len : Int, char : Char = ' ') : Nil diff --git a/src/uri.cr b/src/uri.cr index 9e079f0017e2..d68789bf0b03 100644 --- a/src/uri.cr +++ b/src/uri.cr @@ -282,9 +282,9 @@ class URI # # ``` # uri = URI.parse "http://user:pass@example.com:80/path?query" - # uri.authority # => "user:pass@example.com" + # uri.authority # => "user:pass@example.com:80" # - # uri = URI.parse(path: "/relative") + # uri = URI.parse("/relative") # uri.authority # => nil # ``` def authority : String? diff --git a/src/yaml/to_yaml.cr b/src/yaml/to_yaml.cr index 651072a0b83f..3606f8040ef8 100644 --- a/src/yaml/to_yaml.cr +++ b/src/yaml/to_yaml.cr @@ -149,9 +149,9 @@ struct Enum # RIGHT # end # - # Sides::LEFT.to_yaml # => %(--- ["left"]\n) - # (Sides::LEFT | Sides::RIGHT).to_yaml # => %(--- ["left", "right"]\n) - # Sides::All.to_yaml # => %(--- ["left", "right"]\n) + # Sides::LEFT.to_yaml # => %(--- [left]\n) + # (Sides::LEFT | Sides::RIGHT).to_yaml # => %(--- [left, right]\n) + # Sides::All.to_yaml # => %(--- [left, right]\n) # Sides::None.to_yaml # => %(--- []\n) # ``` # @@ -198,8 +198,8 @@ module Enum::ValueConverter(T) # SECOND_STAGE # end # - # Enum::ValueConverter.to_yaml(Stages::INITIAL) # => %(0) - # Enum::ValueConverter.to_yaml(Stages::SECOND_STAGE) # => %(1) + # Enum::ValueConverter.to_yaml(Stages::INITIAL) # => %(--- 0\n) + # Enum::ValueConverter.to_yaml(Stages::SECOND_STAGE) # => %(--- 1\n) # # @[Flags] # enum Sides @@ -207,10 +207,10 @@ module Enum::ValueConverter(T) # RIGHT # end # - # Enum::ValueConverter.to_yaml(Sides::LEFT) # => %(1) - # Enum::ValueConverter.to_yaml(Sides::LEFT | Sides::RIGHT) # => %(3) - # Enum::ValueConverter.to_yaml(Sides::All) # => %(3) - # Enum::ValueConverter.to_yaml(Sides::None) # => %(0) + # Enum::ValueConverter.to_yaml(Sides::LEFT) # => %(--- 1\n) + # Enum::ValueConverter.to_yaml(Sides::LEFT | Sides::RIGHT) # => %(--- 3\n) + # Enum::ValueConverter.to_yaml(Sides::All) # => %(--- 3\n) + # Enum::ValueConverter.to_yaml(Sides::None) # => %(--- 0\n) # ``` # # `Enum#to_yaml` offers a different serialization strategy based on the member From 2698c43d0be6ef99f4a9e6dd252767edce807621 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Mon, 15 Mar 2021 22:41:14 +0800 Subject: [PATCH 17/33] Don't merge NamedTuple metaclasses through instance types (#10501) --- spec/compiler/semantic/named_tuple_spec.cr | 11 +++++++++++ src/compiler/crystal/semantic/type_merge.cr | 3 +++ 2 files changed, 14 insertions(+) diff --git a/spec/compiler/semantic/named_tuple_spec.cr b/spec/compiler/semantic/named_tuple_spec.cr index f45f19cfbbce..acad0c61b832 100644 --- a/spec/compiler/semantic/named_tuple_spec.cr +++ b/spec/compiler/semantic/named_tuple_spec.cr @@ -332,4 +332,15 @@ describe "Semantic: named tuples" do call("") )) { int32 } end + + it "doesn't unify named tuple metaclasses (#5384)" do + assert_type(%( + NamedTuple(a: Int32) || NamedTuple(a: String) + )) { + union_of( + named_tuple_of({"a": int32}).metaclass, + named_tuple_of({"a": string}).metaclass, + ) + } + end end diff --git a/src/compiler/crystal/semantic/type_merge.cr b/src/compiler/crystal/semantic/type_merge.cr index 761653e272ca..bd9f034e9d85 100644 --- a/src/compiler/crystal/semantic/type_merge.cr +++ b/src/compiler/crystal/semantic/type_merge.cr @@ -249,6 +249,9 @@ module Crystal # Tuple instances might be unified, but never tuple metaclasses return nil if instance_type.is_a?(TupleInstanceType) || other.instance_type.is_a?(TupleInstanceType) + # NamedTuple instances might be unified, but never named tuple metaclasses + return nil if instance_type.is_a?(NamedTupleInstanceType) || other.instance_type.is_a?(NamedTupleInstanceType) + common = instance_type.common_ancestor(other.instance_type) common.try &.metaclass end From 9e049e70ef7029f01d0a1219b747a37984c399fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Mon, 15 Mar 2021 18:49:32 +0100 Subject: [PATCH 18/33] [CI] Fix test_macos with recent bash for nix-shell (#10480) * [CI] Initialize nix environment in prepare_build * [CI] Install recent bash for nix-shell on macos --- bin/ci | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bin/ci b/bin/ci index bdb4d53342d7..05cca928eaa1 100755 --- a/bin/ci +++ b/bin/ci @@ -149,6 +149,14 @@ prepare_build() { on_osx brew update --preinstall on_osx brew bundle --no-lock + # Install a recent bash version for nix-shell. + # macos ships with an ancient one. + if [ `uname` = "Darwin" ]; then + on_nix_shell "brew install bash" + fi + # initialize nix environment + on_nix_shell nix-shell + # Note: brew link --force might show: # Warning: Refusing to link macOS-provided software: llvm # From 17f153eae61225957ed67816ab13f6aeac5b7d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Tue, 16 Mar 2021 15:25:48 +0100 Subject: [PATCH 19/33] Remove implicit `path=/` from `HTTP::Cookie` (#10491) --- spec/std/http/client/response_spec.cr | 2 +- spec/std/http/cookie_spec.cr | 63 ++++++++++++++------------- spec/std/http/server/response_spec.cr | 4 +- src/http/cookie.cr | 6 +-- 4 files changed, 39 insertions(+), 36 deletions(-) diff --git a/spec/std/http/client/response_spec.cr b/spec/std/http/client/response_spec.cr index 29b839f21c2e..311259f6aa9e 100644 --- a/spec/std/http/client/response_spec.cr +++ b/spec/std/http/client/response_spec.cr @@ -223,7 +223,7 @@ class HTTP::Client::Response io.clear response.to_io(io) - io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\nSet-Cookie: foo=baz; path=/\r\nSet-Cookie: quux=baz; path=/\r\n\r\nhello") + io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\nSet-Cookie: foo=baz\r\nSet-Cookie: quux=baz\r\n\r\nhello") end it "sets content length from body" do diff --git a/spec/std/http/cookie_spec.cr b/spec/std/http/cookie_spec.cr index 9ac457ed6ecd..46fb5508c540 100644 --- a/spec/std/http/cookie_spec.cr +++ b/spec/std/http/cookie_spec.cr @@ -98,21 +98,24 @@ module HTTP end describe "#to_set_cookie_header" do - it { HTTP::Cookie.new("x", "v$1").to_set_cookie_header.should eq "x=v$1; path=/" } + it { HTTP::Cookie.new("x", "v$1").to_set_cookie_header.should eq "x=v$1" } - it { HTTP::Cookie.new("x", "seven", domain: "127.0.0.1").to_set_cookie_header.should eq "x=seven; domain=127.0.0.1; path=/" } + it { HTTP::Cookie.new("x", "seven", domain: "127.0.0.1").to_set_cookie_header.should eq "x=seven; domain=127.0.0.1" } - it { HTTP::Cookie.new("x", "expiring", expires: Time.unix(1257894000)).to_set_cookie_header.should eq "x=expiring; path=/; expires=Tue, 10 Nov 2009 23:00:00 GMT" } - it { HTTP::Cookie.new("x", "expiring-1601", expires: Time.utc(1601, 1, 1, 1, 1, 1, nanosecond: 1)).to_set_cookie_header.should eq "x=expiring-1601; path=/; expires=Mon, 01 Jan 1601 01:01:01 GMT" } + it { HTTP::Cookie.new("x", "y", path: "/").to_set_cookie_header.should eq "x=y; path=/" } + it { HTTP::Cookie.new("x", "y", path: "/example").to_set_cookie_header.should eq "x=y; path=/example" } + + it { HTTP::Cookie.new("x", "expiring", expires: Time.unix(1257894000)).to_set_cookie_header.should eq "x=expiring; expires=Tue, 10 Nov 2009 23:00:00 GMT" } + it { HTTP::Cookie.new("x", "expiring-1601", expires: Time.utc(1601, 1, 1, 1, 1, 1, nanosecond: 1)).to_set_cookie_header.should eq "x=expiring-1601; expires=Mon, 01 Jan 1601 01:01:01 GMT" } it "samesite" do - HTTP::Cookie.new("x", "samesite-default", samesite: nil).to_set_cookie_header.should eq "x=samesite-default; path=/" - HTTP::Cookie.new("x", "samesite-lax", samesite: :lax).to_set_cookie_header.should eq "x=samesite-lax; path=/; SameSite=Lax" - HTTP::Cookie.new("x", "samesite-strict", samesite: :strict).to_set_cookie_header.should eq "x=samesite-strict; path=/; SameSite=Strict" - HTTP::Cookie.new("x", "samesite-none", samesite: :none).to_set_cookie_header.should eq "x=samesite-none; path=/; SameSite=None" + HTTP::Cookie.new("x", "samesite-default", samesite: nil).to_set_cookie_header.should eq "x=samesite-default" + HTTP::Cookie.new("x", "samesite-lax", samesite: :lax).to_set_cookie_header.should eq "x=samesite-lax; SameSite=Lax" + HTTP::Cookie.new("x", "samesite-strict", samesite: :strict).to_set_cookie_header.should eq "x=samesite-strict; SameSite=Strict" + HTTP::Cookie.new("x", "samesite-none", samesite: :none).to_set_cookie_header.should eq "x=samesite-none; SameSite=None" end - it { HTTP::Cookie.new("empty-value", "").to_set_cookie_header.should eq "empty-value=; path=/" } + it { HTTP::Cookie.new("empty-value", "").to_set_cookie_header.should eq "empty-value=" } end end @@ -122,7 +125,7 @@ module HTTP cookie = parse_first_cookie("key=value") cookie.name.should eq("key") cookie.value.should eq("value") - cookie.to_set_cookie_header.should eq("key=value; path=/") + cookie.to_set_cookie_header.should eq("key=value") end it "parse_set_cookie with space" do @@ -135,28 +138,28 @@ module HTTP cookie = parse_first_cookie("key=") cookie.name.should eq("key") cookie.value.should eq("") - cookie.to_set_cookie_header.should eq("key=; path=/") + cookie.to_set_cookie_header.should eq("key=") end it "parses key=key=value" do cookie = parse_first_cookie("key=key=value") cookie.name.should eq("key") cookie.value.should eq("key=value") - cookie.to_set_cookie_header.should eq("key=key=value; path=/") + cookie.to_set_cookie_header.should eq("key=key=value") end it "parses key=key%3Dvalue" do cookie = parse_first_cookie("key=key%3Dvalue") cookie.name.should eq("key") cookie.value.should eq("key%3Dvalue") - cookie.to_set_cookie_header.should eq("key=key%3Dvalue; path=/") + cookie.to_set_cookie_header.should eq("key=key%3Dvalue") end it "parses special character in name" do cookie = parse_first_cookie("key%3Dvalue=value") cookie.name.should eq("key%3Dvalue") cookie.value.should eq("value") - cookie.to_set_cookie_header.should eq("key%3Dvalue=value; path=/") + cookie.to_set_cookie_header.should eq("key%3Dvalue=value") end it "parses multiple cookies" do @@ -184,7 +187,7 @@ module HTTP cookie.name.should eq("key") cookie.value.should eq("value") cookie.secure.should be_true - cookie.to_set_cookie_header.should eq("key=value; path=/; Secure") + cookie.to_set_cookie_header.should eq("key=value; Secure") end it "parses HttpOnly" do @@ -192,7 +195,7 @@ module HTTP cookie.name.should eq("key") cookie.value.should eq("value") cookie.http_only.should be_true - cookie.to_set_cookie_header.should eq("key=value; path=/; HttpOnly") + cookie.to_set_cookie_header.should eq("key=value; HttpOnly") end describe "SameSite" do @@ -202,7 +205,7 @@ module HTTP cookie.name.should eq "key" cookie.value.should eq "value" cookie.samesite.should eq HTTP::Cookie::SameSite::Lax - cookie.to_set_cookie_header.should eq "key=value; path=/; SameSite=Lax" + cookie.to_set_cookie_header.should eq "key=value; SameSite=Lax" end end @@ -212,7 +215,7 @@ module HTTP cookie.name.should eq "key" cookie.value.should eq "value" cookie.samesite.should eq HTTP::Cookie::SameSite::Strict - cookie.to_set_cookie_header.should eq "key=value; path=/; SameSite=Strict" + cookie.to_set_cookie_header.should eq "key=value; SameSite=Strict" end end @@ -222,7 +225,7 @@ module HTTP cookie.name.should eq "key" cookie.value.should eq "value" cookie.samesite.should be_nil - cookie.to_set_cookie_header.should eq "key=value; path=/" + cookie.to_set_cookie_header.should eq "key=value" end end end @@ -232,7 +235,7 @@ module HTTP cookie.name.should eq("key") cookie.value.should eq("value") cookie.domain.should eq("www.example.com") - cookie.to_set_cookie_header.should eq("key=value; domain=www.example.com; path=/") + cookie.to_set_cookie_header.should eq("key=value; domain=www.example.com") end it "parses expires iis" do @@ -291,7 +294,7 @@ module HTTP end it "parse domain as IP" do - parse_set_cookie("a=1; domain=127.0.0.1; path=/; HttpOnly").domain.should eq "127.0.0.1" + parse_set_cookie("a=1; domain=127.0.0.1; HttpOnly").domain.should eq "127.0.0.1" end it "parse max-age as seconds from current time" do @@ -450,20 +453,20 @@ module HTTP describe "adding response headers" do it "overwrites all pre-existing Set-Cookie headers" do headers = Headers.new - headers.add("Set-Cookie", "a=b; path=/") - headers.add("Set-Cookie", "c=d; path=/") + headers.add("Set-Cookie", "a=b") + headers.add("Set-Cookie", "c=d") cookies = Cookies.new cookies << Cookie.new("x", "y") headers.get("Set-Cookie").size.should eq 2 - headers.get("Set-Cookie").includes?("a=b; path=/").should be_true - headers.get("Set-Cookie").includes?("c=d; path=/").should be_true + headers.get("Set-Cookie").includes?("a=b").should be_true + headers.get("Set-Cookie").includes?("c=d").should be_true cookies.add_response_headers(headers) headers.get("Set-Cookie").size.should eq 1 - headers.get("Set-Cookie")[0].should eq "x=y; path=/" + headers.get("Set-Cookie")[0].should eq "x=y" end it "sets one Set-Cookie header per cookie" do @@ -476,8 +479,8 @@ module HTTP cookies.add_response_headers(headers) headers.get?("Set-Cookie").should_not be_nil - headers.get("Set-Cookie").includes?("a=b; path=/").should be_true - headers.get("Set-Cookie").includes?("c=d; path=/").should be_true + headers.get("Set-Cookie").includes?("a=b").should be_true + headers.get("Set-Cookie").includes?("c=d").should be_true end it "uses encode_www_form on Set-Cookie value" do @@ -485,13 +488,13 @@ module HTTP cookies = Cookies.new cookies << Cookie.new("a", "b+c") cookies.add_response_headers(headers) - headers.get("Set-Cookie").includes?("a=b+c; path=/").should be_true + headers.get("Set-Cookie").includes?("a=b+c").should be_true end describe "when no cookies are set" do it "does not set a Set-Cookie header" do headers = Headers.new - headers.add("Set-Cookie", "a=b; path=/") + headers.add("Set-Cookie", "a=b") cookies = Cookies.new headers.get?("Set-Cookie").should_not be_nil diff --git a/spec/std/http/server/response_spec.cr b/spec/std/http/server/response_spec.cr index 15353f6bea3b..b9388bc01eaf 100644 --- a/spec/std/http/server/response_spec.cr +++ b/spec/std/http/server/response_spec.cr @@ -217,14 +217,14 @@ describe HTTP::Server::Response do response = Response.new(io) response.cookies["Bar"] = "Foo" response.close - io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 0\r\nSet-Cookie: Bar=Foo; path=/\r\n\r\n") + io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 0\r\nSet-Cookie: Bar=Foo\r\n\r\n") io = IO::Memory.new response = Response.new(io) response.cookies["Bar"] = "Foo" response.print("Hello") response.close - io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 5\r\nSet-Cookie: Bar=Foo; path=/\r\n\r\nHello") + io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 5\r\nSet-Cookie: Bar=Foo\r\n\r\nHello") end it "closes when it fails to write" do diff --git a/src/http/cookie.cr b/src/http/cookie.cr index d4f6de7c365e..c86a16bdf7e7 100644 --- a/src/http/cookie.cr +++ b/src/http/cookie.cr @@ -17,7 +17,7 @@ module HTTP getter name : String getter value : String - property path : String + property path : String? property expires : Time? property domain : String? property secure : Bool @@ -30,7 +30,7 @@ module HTTP # Creates a new `Cookie` instance. # # Raises `IO::Error` if *name* or *value* are invalid as per [RFC 6265 §4.1.1](https://tools.ietf.org/html/rfc6265#section-4.1.1). - def initialize(name : String, value : String, @path : String = "/", + def initialize(name : String, value : String, @path : String? = nil, @expires : Time? = nil, @domain : String? = nil, @secure : Bool = false, @http_only : Bool = false, @samesite : SameSite? = nil, @extension : String? = nil) @@ -174,7 +174,7 @@ module HTTP Cookie.new( match["name"], match["value"], - path: match["path"]? || "/", + path: match["path"]?, expires: expires, domain: match["domain"]?, secure: match["secure"]? != nil, From e572b567518502b9521b32b4228e4df0a6760e66 Mon Sep 17 00:00:00 2001 From: Stephanie Wilde-Hobbs Date: Tue, 16 Mar 2021 14:43:00 +0000 Subject: [PATCH 20/33] Support closured vars inside Const initializer (#10478) * Support Const initializer typed Nil * Support closured vars inside Const initializer * fixup! Support Const initializer typed Nil * fixup! Support Const initializer typed Nil --- spec/compiler/codegen/const_spec.cr | 35 +++++++++++++++++++ src/compiler/crystal/codegen/const.cr | 8 +++-- src/compiler/crystal/semantic/main_visitor.cr | 2 +- src/compiler/crystal/types.cr | 2 +- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/spec/compiler/codegen/const_spec.cr b/spec/compiler/codegen/const_spec.cr index d7da646a2af0..19d1eeced16e 100644 --- a/spec/compiler/codegen/const_spec.cr +++ b/spec/compiler/codegen/const_spec.cr @@ -533,4 +533,39 @@ describe "Codegen: const" do a &+ Foo.x )).to_i.should eq(6) end + + it "supports closured vars inside initializers (#10474)" do + run(%( + class Foo + def bar + 3 + end + end + + def func(&block : -> Int32) + block.call + end + + CONST = begin + foo = Foo.new + func do + foo.bar + end + end + + CONST + )).to_i.should eq(3) + end + + it "supports storing function returning nil" do + run(%( + def foo + "foo" + nil + end + + CONST = foo + CONST.nil? + )).to_b.should eq(true) + end end diff --git a/src/compiler/crystal/codegen/const.cr b/src/compiler/crystal/codegen/const.cr index ed6532c88738..48a053619a60 100644 --- a/src/compiler/crystal/codegen/const.cr +++ b/src/compiler/crystal/codegen/const.cr @@ -100,7 +100,7 @@ class Crystal::CodeGenVisitor # Start with fresh variables context.vars = LLVMVars.new - alloca_vars const.vars + alloca_vars const.fake_def.try(&.vars), const.fake_def request_value do accept const.value end @@ -153,7 +153,7 @@ class Crystal::CodeGenVisitor # Start with fresh variables context.vars = LLVMVars.new - alloca_vars const.vars + alloca_vars const.fake_def.try(&.vars), const.fake_def request_value do accept const.value @@ -173,7 +173,9 @@ class Crystal::CodeGenVisitor end else global.initializer = llvm_type(const.value.type).null - store @last, global + unless const.value.type.nil_type? || const.value.type.void? + store @last, global + end end ret diff --git a/src/compiler/crystal/semantic/main_visitor.cr b/src/compiler/crystal/semantic/main_visitor.cr index b6e07086fcde..3807af827c38 100644 --- a/src/compiler/crystal/semantic/main_visitor.cr +++ b/src/compiler/crystal/semantic/main_visitor.cr @@ -189,7 +189,7 @@ module Crystal type_visitor.inside_constant = true type.value.accept type_visitor - type.vars = const_def.vars + type.fake_def = const_def type.visitor = self type.used = true diff --git a/src/compiler/crystal/types.cr b/src/compiler/crystal/types.cr index fac6fe8a7557..11494bdaf7d4 100644 --- a/src/compiler/crystal/types.cr +++ b/src/compiler/crystal/types.cr @@ -3155,7 +3155,7 @@ module Crystal # saved under a type types like any other type. class Const < NamedType property value : ASTNode - property vars : MetaVars? + property fake_def : Def? property? used = false property? visited = false From 3386930662e1311ce61ae32797aef333ec8f4d50 Mon Sep 17 00:00:00 2001 From: Jamie Gaskins Date: Fri, 19 Mar 2021 10:41:12 -0400 Subject: [PATCH 21/33] Add IO::Sized#remaining=(value) to reuse an existing instance (#10520) * Add IO::Sized#read_remaining=(value) * Actually extend the IO::Sized#read_remaining --- spec/std/io/sized_spec.cr | 14 ++++++++++++++ src/io/sized.cr | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/spec/std/io/sized_spec.cr b/spec/std/io/sized_spec.cr index 8338063ff9db..653cdc189761 100644 --- a/spec/std/io/sized_spec.cr +++ b/spec/std/io/sized_spec.cr @@ -52,6 +52,20 @@ describe "IO::Sized" do String.new(slice).should eq("12345\0\0\0\0\0") end + it "allows extending the size" do + io = IO::Memory.new("1234567890") + sized = IO::Sized.new(io, read_size: 5) + slice = Bytes.new(10) + + sized.read(slice).should eq(5) + String.new(slice).should eq("12345\0\0\0\0\0") + + sized.read_remaining = 5 + + sized.read(slice).should eq(5) + String.new(slice).should eq("67890\0\0\0\0\0") + end + it "raises on negative numbers" do io = IO::Memory.new expect_raises(ArgumentError, "Negative read_size") do diff --git a/src/io/sized.cr b/src/io/sized.cr index 253df834a743..40e2e49ad988 100644 --- a/src/io/sized.cr +++ b/src/io/sized.cr @@ -13,7 +13,7 @@ class IO::Sized < IO property? sync_close : Bool # The number of remaining bytes to be read. - getter read_remaining : UInt64 + property read_remaining : UInt64 getter? closed : Bool # Creates a new `IO::Sized` which wraps *io*, and can read a maximum of From 395d0bf5173e51fc18f03d3854e1cd456b195c90 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Fri, 19 Mar 2021 22:43:04 +0800 Subject: [PATCH 22/33] Disallow surrogate halves in string and char literals (#10443) --- spec/compiler/lexer/lexer_spec.cr | 4 ++++ spec/compiler/lexer/lexer_string_spec.cr | 4 ++++ spec/std/string/utf16_spec.cr | 2 +- src/compiler/crystal/syntax/lexer.cr | 5 +++++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/spec/compiler/lexer/lexer_spec.cr b/spec/compiler/lexer/lexer_spec.cr index a0b02a608b1b..6a858baeaae8 100644 --- a/spec/compiler/lexer/lexer_spec.cr +++ b/spec/compiler/lexer/lexer_spec.cr @@ -528,6 +528,10 @@ describe "Lexer" do assert_syntax_error "'\\uFEDZ'", "expected hexadecimal character in unicode escape" assert_syntax_error "'\\u{}'", "expected hexadecimal character in unicode escape" assert_syntax_error "'\\u{110000}'", "invalid unicode codepoint (too large)" + assert_syntax_error "'\\uD800'", "invalid unicode codepoint (surrogate half)" + assert_syntax_error "'\\uDFFF'", "invalid unicode codepoint (surrogate half)" + assert_syntax_error "'\\u{D800}'", "invalid unicode codepoint (surrogate half)" + assert_syntax_error "'\\u{DFFF}'", "invalid unicode codepoint (surrogate half)" assert_syntax_error ":+1", "unexpected token" assert_syntax_error "'\\1'", "invalid char escape sequence" diff --git a/spec/compiler/lexer/lexer_string_spec.cr b/spec/compiler/lexer/lexer_string_spec.cr index 8e4102d0053b..f2f7c10d5ef3 100644 --- a/spec/compiler/lexer/lexer_string_spec.cr +++ b/spec/compiler/lexer/lexer_string_spec.cr @@ -288,6 +288,10 @@ describe "Lexer string" do assert_syntax_error "\"\\uFEDZ\"", "expected hexadecimal character in unicode escape" assert_syntax_error "\"\\u{}\"", "expected hexadecimal character in unicode escape" assert_syntax_error "\"\\u{110000}\"", "invalid unicode codepoint (too large)" + assert_syntax_error "\"\\uD800\"", "invalid unicode codepoint (surrogate half)" + assert_syntax_error "\"\\uDFFF\"", "invalid unicode codepoint (surrogate half)" + assert_syntax_error "\"\\u{D800}\"", "invalid unicode codepoint (surrogate half)" + assert_syntax_error "\"\\u{DFFF}\"", "invalid unicode codepoint (surrogate half)" it "lexes backtick string" do lexer = Lexer.new(%(`hello`)) diff --git a/spec/std/string/utf16_spec.cr b/spec/std/string/utf16_spec.cr index 912772ea050b..49141c79ddd2 100644 --- a/spec/std/string/utf16_spec.cr +++ b/spec/std/string/utf16_spec.cr @@ -27,7 +27,7 @@ describe "String UTF16" do end it "in the range U+D800..U+DFFF" do - encoded = "\u{D800}\u{DFFF}".to_utf16 + encoded = String.new(Bytes[0xED, 0xA0, 0x80, 0xED, 0xBF, 0xBF]).to_utf16 encoded.should eq(Slice[0xFFFD_u16, 0xFFFD_u16, 0xFFFD_u16, 0xFFFD_u16, 0xFFFD_u16, 0xFFFD_u16]) encoded.unsafe_fetch(encoded.size).should eq 0_u16 end diff --git a/src/compiler/crystal/syntax/lexer.cr b/src/compiler/crystal/syntax/lexer.cr index e21c660602ed..121e01a326e2 100644 --- a/src/compiler/crystal/syntax/lexer.cr +++ b/src/compiler/crystal/syntax/lexer.cr @@ -2739,6 +2739,9 @@ module Crystal hex_value = char_to_hex(next_char) { expected_hexacimal_character_in_unicode_escape } codepoint = 16 * codepoint + hex_value end + if 0xD800 <= codepoint <= 0xDFFF + raise "invalid unicode codepoint (surrogate half)" + end codepoint end @@ -2773,6 +2776,8 @@ module Crystal expected_hexacimal_character_in_unicode_escape elsif codepoint > 0x10FFFF raise "invalid unicode codepoint (too large)" + elsif 0xD800 <= codepoint <= 0xDFFF + raise "invalid unicode codepoint (surrogate half)" end unless found_space From bbaabb21784134e874d8f5eb55dc4858e0c295bb Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Fri, 19 Mar 2021 22:44:34 +0800 Subject: [PATCH 23/33] Support splats inside tuple literals in type names (#10430) --- spec/compiler/formatter/formatter_spec.cr | 1 + spec/compiler/parser/parser_spec.cr | 1 + src/compiler/crystal/syntax/parser.cr | 12 ++++++++---- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/spec/compiler/formatter/formatter_spec.cr b/spec/compiler/formatter/formatter_spec.cr index 76cd16bb7a03..4541df4d0c99 100644 --- a/spec/compiler/formatter/formatter_spec.cr +++ b/spec/compiler/formatter/formatter_spec.cr @@ -107,6 +107,7 @@ describe Crystal::Formatter do assert_format "Foo( A , 1 )", "Foo(A, 1)" assert_format "Foo( x: Int32 )", "Foo(x: Int32)" assert_format "Foo( x: Int32 , y: Float64 )", "Foo(x: Int32, y: Float64)" + assert_format "Foo( * T, { * A ,*\n B } )", "Foo(*T, {*A, *B})" assert_format "NamedTuple(a: Int32,)", "NamedTuple(a: Int32)" assert_format "NamedTuple(\n a: Int32,\n)" diff --git a/spec/compiler/parser/parser_spec.cr b/spec/compiler/parser/parser_spec.cr index 5bdbf690b6f2..e0b21d4a3b2a 100644 --- a/spec/compiler/parser/parser_spec.cr +++ b/spec/compiler/parser/parser_spec.cr @@ -569,6 +569,7 @@ module Crystal it_parses "Foo(typeof(1), typeof(2))", Generic.new("Foo".path, [TypeOf.new([1.int32] of ASTNode), TypeOf.new([2.int32] of ASTNode)] of ASTNode) it_parses "Foo({X, Y})", Generic.new("Foo".path, [Generic.new(Path.global("Tuple"), ["X".path, "Y".path] of ASTNode)] of ASTNode) it_parses "Foo({X, Y,})", Generic.new("Foo".path, [Generic.new(Path.global("Tuple"), ["X".path, "Y".path] of ASTNode)] of ASTNode) + it_parses "Foo({*X, *{Y}})", Generic.new("Foo".path, [Generic.new(Path.global("Tuple"), ["X".path.splat, Generic.new(Path.global("Tuple"), ["Y".path] of ASTNode).splat] of ASTNode)] of ASTNode) it_parses "Foo({->})", Generic.new("Foo".path, [Generic.new(Path.global("Tuple"), [ProcNotation.new] of ASTNode)] of ASTNode) it_parses "Foo({String, ->})", Generic.new("Foo".path, [Generic.new(Path.global("Tuple"), ["String".path, ProcNotation.new] of ASTNode)] of ASTNode) it_parses "Foo({String, ->, ->})", Generic.new("Foo".path, [Generic.new(Path.global("Tuple"), ["String".path, ProcNotation.new, ProcNotation.new] of ASTNode)] of ASTNode) diff --git a/src/compiler/crystal/syntax/parser.cr b/src/compiler/crystal/syntax/parser.cr index 0f9e58e83632..075ff20303f1 100644 --- a/src/compiler/crystal/syntax/parser.cr +++ b/src/compiler/crystal/syntax/parser.cr @@ -4788,7 +4788,7 @@ module Crystal if named_tuple_start? || @token.type == :DELIMITER_START type = make_named_tuple_type parse_named_type_args(:"}") else - type = make_tuple_type parse_union_types(:"}") + type = make_tuple_type parse_union_types(:"}", allow_splats: true) end check :"}" next_token_skip_space @@ -4828,13 +4828,17 @@ module Crystal end end - def parse_union_types(end_token) - types = [parse_union_type] + def parse_union_types(end_token, *, allow_splats = false) + type = allow_splats ? parse_type_splat { parse_union_type } : parse_union_type + types = [type] + while @token.type == :"," next_token_skip_space_or_newline break if @token.type == end_token # allow trailing comma - types << parse_union_type + type = allow_splats ? parse_type_splat { parse_union_type } : parse_union_type + types << type end + types end From 11658e3bc4cf833aade2b95a879a75085f6213af Mon Sep 17 00:00:00 2001 From: Sijawusz Pur Rahnama Date: Fri, 19 Mar 2021 15:46:33 +0100 Subject: [PATCH 24/33] Add llvm 11.1 to the list of supported versions (#10523) --- src/llvm/ext/llvm-versions.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/llvm/ext/llvm-versions.txt b/src/llvm/ext/llvm-versions.txt index d8870d4b0899..2fdbdf9a1c98 100644 --- a/src/llvm/ext/llvm-versions.txt +++ b/src/llvm/ext/llvm-versions.txt @@ -1 +1 @@ -11.0 10.0 9.0 8.0 7.1 6.0 5.0 4.0 3.9 3.8 +11.1 11.0 10.0 9.0 8.0 7.1 6.0 5.0 4.0 3.9 3.8 From f991977999e61fc18922fc00c3582552d9bf1ec9 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Fri, 19 Mar 2021 23:06:15 +0800 Subject: [PATCH 25/33] Accept pointer types on falsey conditional branches (#10464) --- spec/compiler/semantic/if_spec.cr | 16 ++++++++++++++++ src/compiler/crystal/semantic/filters.cr | 10 +++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/spec/compiler/semantic/if_spec.cr b/spec/compiler/semantic/if_spec.cr index ecaff295d98a..862320875595 100644 --- a/spec/compiler/semantic/if_spec.cr +++ b/spec/compiler/semantic/if_spec.cr @@ -422,4 +422,20 @@ describe "Semantic: if" do foo ), inject_primitives: false) { int32 } end + + it "includes pointer types in falsey branch" do + assert_type(%( + def foo + x = 1 + y = false || pointerof(x) || nil + + if !y + return y + end + 1 + end + + foo + )) { nilable union_of bool, pointer_of(int32), int32 } + end end diff --git a/src/compiler/crystal/semantic/filters.cr b/src/compiler/crystal/semantic/filters.cr index 17c9e72fc70c..3543ccdb080b 100644 --- a/src/compiler/crystal/semantic/filters.cr +++ b/src/compiler/crystal/semantic/filters.cr @@ -189,9 +189,13 @@ module Crystal resulting_types = other_types - types - # Special case: not truthy (falsey) can also be bool - if @filter.is_a?(TruthyFilter) && (bool_type = types.find(&.bool_type?)) - resulting_types << bool_type + # Special case: not truthy (falsey) can also be bool or pointer + if @filter.is_a?(TruthyFilter) + types.each do |type| + if type.bool_type? || type.pointer? + resulting_types << type + end + end end case resulting_types.size From 917d41ca2f6fece1e01807d103fa4c5e9d303f7d Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Mon, 22 Mar 2021 21:24:07 +0800 Subject: [PATCH 26/33] Match named args by external names when checking overloads (#10530) --- spec/compiler/semantic/def_overload_spec.cr | 11 +++++++++++ src/compiler/crystal/semantic/cover.cr | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/spec/compiler/semantic/def_overload_spec.cr b/spec/compiler/semantic/def_overload_spec.cr index 1921205f3179..3bc8c8d4d56a 100644 --- a/spec/compiler/semantic/def_overload_spec.cr +++ b/spec/compiler/semantic/def_overload_spec.cr @@ -892,6 +892,17 @@ describe "Semantic: def overload" do "no overload matches" end + it "errors if no overload matches on union against named arg with external param name (#10516)" do + assert_error %( + def f(a b : Int32) + end + + a = 1 || nil + f(a: a) + ), + "no overload matches" + end + it "dispatches with named arg" do assert_type(%( def f(a : Int32, b : Int32) diff --git a/src/compiler/crystal/semantic/cover.cr b/src/compiler/crystal/semantic/cover.cr index 2d38312a8d50..b2865decfca8 100644 --- a/src/compiler/crystal/semantic/cover.cr +++ b/src/compiler/crystal/semantic/cover.cr @@ -63,7 +63,7 @@ module Crystal end signature.named_args.try &.each_with_index do |named_arg, i| - arg = match.def.args.find(&.name.==(named_arg.name)) + arg = match.def.args.find(&.external_name.==(named_arg.name)) if arg && (arg.type? || arg.restriction) indices[args_size + i] = true end From dd40a2442fa186add8a82b74edb14a90aa1dae05 Mon Sep 17 00:00:00 2001 From: "Brian J. Cardiff" Date: Mon, 22 Mar 2021 11:07:01 -0300 Subject: [PATCH 27/33] Release 1.0.0 (#10500) --- CHANGELOG.md | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- src/VERSION | 2 +- 3 files changed, 116 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c72668ed97f6..f1e975e8a97e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,117 @@ +# 1.0.0 (2021-03-22) + +## Language changes + +- Support `Tuple#[](Range)` with compile-time range literals. ([#10379](https://github.com/crystal-lang/crystal/pull/10379), thanks @HertzDevil) + +### Macros + +- Don't use named argument key names as parameters for `method_missing` calls. ([#10388](https://github.com/crystal-lang/crystal/pull/10388), thanks @HertzDevil) + +## Standard library + +- **(breaking-change)** Drop deprecated definitions. ([#10386](https://github.com/crystal-lang/crystal/pull/10386), thanks @bcardiff) +- Fix example codes in multiple places. ([#10505](https://github.com/crystal-lang/crystal/pull/10505), thanks @maiha) + +### Macros + +- **(breaking-change)** Always add explicit return types in getter/property macros. ([#10405](https://github.com/crystal-lang/crystal/pull/10405), thanks @Sija) + +### Numeric + +- **(breaking-change)** Change default rounding mode to `TIES_EVEN`. ([#10508](https://github.com/crystal-lang/crystal/pull/10508), thanks @straight-shoota) +- Fix downcasting float infinity. ([#10420](https://github.com/crystal-lang/crystal/pull/10420), thanks @straight-shoota) +- Fix `String#to_f` out of range behaviour. ([#10425](https://github.com/crystal-lang/crystal/pull/10425), thanks @straight-shoota) +- Implement rounding mode for `Number#round`. ([#10413](https://github.com/crystal-lang/crystal/pull/10413), [#10360](https://github.com/crystal-lang/crystal/pull/10360), [#10479](https://github.com/crystal-lang/crystal/pull/10479), thanks @straight-shoota) + +### Text + +- Add missing unicode whitespace support to `String` methods. ([#10367](https://github.com/crystal-lang/crystal/pull/10367), thanks @straight-shoota) + +### Collections + +- Fix `Range#==` to ignore generic type arguments. ([#10309](https://github.com/crystal-lang/crystal/pull/10309), thanks @straight-shoota) +- Make `Enumerable#flat_map`, `Iterator#flat_map` work with mixed element types. ([#10329](https://github.com/crystal-lang/crystal/pull/10329), thanks @HertzDevil) +- Remove duplicated `sort` related specs. ([#10208](https://github.com/crystal-lang/crystal/pull/10208), thanks @MakeNowJust) +- Fix docs regarding `Set#each` return type. ([#10477](https://github.com/crystal-lang/crystal/pull/10477), thanks @kachick) +- Fix docs examples regarding `Set#*set_of?`. ([#10285](https://github.com/crystal-lang/crystal/pull/10285), thanks @oddp) +- Fix expectation on set specs. ([#10482](https://github.com/crystal-lang/crystal/pull/10482), thanks @kachick) + +### Serialization + +- **(breaking-change)** Serialize `Enum` to underscored `String` by default. ([#10431](https://github.com/crystal-lang/crystal/pull/10431), thanks @straight-shoota, @caspiano) +- **(breaking-change)** Use class instead of struct for types in XML module. ([#10436](https://github.com/crystal-lang/crystal/pull/10436), thanks @hugopl) +- Add `YAML::Nodes::Node#kind`. ([#10432](https://github.com/crystal-lang/crystal/pull/10432), thanks @straight-shoota) + +### Files + +- Let `IO::Memory` not be writable with read-only `Slice`. ([#10391](https://github.com/crystal-lang/crystal/pull/10391), thanks @straight-shoota) +- Allow `Int64` values within `IO#read_at`. ([#10356](https://github.com/crystal-lang/crystal/pull/10356), thanks @Blacksmoke16) +- Add `IO::Sized#remaining=(value)` to reuse an existing instance. ([#10520](https://github.com/crystal-lang/crystal/pull/10520), thanks @jgaskins) + +### Networking + +- **(security)** Remove Cookie Name Decoding. ([#10442](https://github.com/crystal-lang/crystal/pull/10442), thanks @security-curious) +- **(breaking-change)** Remove implicit en-/decoding for cookie values. ([#10485](https://github.com/crystal-lang/crystal/pull/10485), thanks @straight-shoota) +- **(breaking-change)** Split `HTTP::Cookies.from_headers` into separate methods for server/client. ([#10486](https://github.com/crystal-lang/crystal/pull/10486), thanks @straight-shoota) +- **(performance)** Minor performance improvements to `HTTP::Cookies`. ([#10488](https://github.com/crystal-lang/crystal/pull/10488), thanks @straight-shoota) +- Respect subclasses when constructing `HTTP::Client` from class methods. ([#10375](https://github.com/crystal-lang/crystal/pull/10375), thanks @oprypin) +- Make the `content-length` header more RFC compliant. ([#10353](https://github.com/crystal-lang/crystal/pull/10353), thanks @Blacksmoke16) +- Fix `#respond_with_status` when headers written or closed. ([#10415](https://github.com/crystal-lang/crystal/pull/10415), thanks @straight-shoota) +- Fix `Cookie#==` to take all ivars into account. ([#10487](https://github.com/crystal-lang/crystal/pull/10487), thanks @straight-shoota) +- Remove implicit `path=/` from `HTTP::Cookie`. ([#10491](https://github.com/crystal-lang/crystal/pull/10491), thanks @straight-shoota) +- Add `HTTP::Request#local_address`. ([#10385](https://github.com/crystal-lang/crystal/pull/10385), thanks @carlhoerberg) + +### Logging + +- Close `AsyncDispatcher` on `#finalize`. ([#10390](https://github.com/crystal-lang/crystal/pull/10390), thanks @straight-shoota) + +### System + +- Fix `Process.parse_argument` behavior against a quote in a word. ([#10337](https://github.com/crystal-lang/crystal/pull/10337), thanks @MakeNowJust) +- Add aarch64 support for macOS/darwin targets. ([#10348](https://github.com/crystal-lang/crystal/pull/10348), thanks @maxfierke, @RomainFranceschini) +- Add `LibC::MAP_ANONYMOUS` to x86_64-darwin to match other platforms. ([#10398](https://github.com/crystal-lang/crystal/pull/10398), thanks @sourgrasses) + +### Runtime + +- Improve error message for ELF reader on uninitialized runtime. ([#10282](https://github.com/crystal-lang/crystal/pull/10282), thanks @straight-shoota) + +## Compiler + +- **(breaking-change)** Disallow surrogate halves in escape sequences of string and character literals, use `\x` for arbitrary binary values. ([#10443](https://github.com/crystal-lang/crystal/pull/10443), thanks @HertzDevil) +- Fix ICE when exhaustive in-clause calls pseudo-method. ([#10382](https://github.com/crystal-lang/crystal/pull/10382), thanks @HertzDevil) +- Fix ICE when parsing `foo.%` calls. ([#10351](https://github.com/crystal-lang/crystal/pull/10351), thanks @MakeNowJust) +- Fix edge cases for symbol quoting rules. ([#10389](https://github.com/crystal-lang/crystal/pull/10389), thanks @HertzDevil) +- Support closured vars inside `Const` initializer. ([#10478](https://github.com/crystal-lang/crystal/pull/10478), thanks @RX14) +- Documentation grammar fix. ([#10369](https://github.com/crystal-lang/crystal/pull/10369), thanks @szTheory) + +### Language semantics + +- Don't fail on untyped `is_a?`. ([#10320](https://github.com/crystal-lang/crystal/pull/10320), thanks @asterite) +- Fix named arguments in `super` and `previous_def` calls. ([#10400](https://github.com/crystal-lang/crystal/pull/10400), thanks @HertzDevil) +- Fix assignments in array literals. ([#10009](https://github.com/crystal-lang/crystal/pull/10009), thanks @straight-shoota) +- Consider type var splats in generic type restrictions. ([#10168](https://github.com/crystal-lang/crystal/pull/10168), thanks @HertzDevil) +- Align `Proc.new(&block)`'s behaviour with other captured blocks. ([#10263](https://github.com/crystal-lang/crystal/pull/10263), thanks @HertzDevil) +- Don't merge `NamedTuple` metaclasses through instance types. ([#10501](https://github.com/crystal-lang/crystal/pull/10501), thanks @HertzDevil) +- Access instantiations of `NamedTuple` and other generics uniformly. ([#10401](https://github.com/crystal-lang/crystal/pull/10401), thanks @HertzDevil) +- Improve error message for auto-cast error at Var assign. ([#10327](https://github.com/crystal-lang/crystal/pull/10327), thanks @straight-shoota) +- Exclude abstract defs from "no overload matches" errors. ([#10483](https://github.com/crystal-lang/crystal/pull/10483), thanks @HertzDevil) +- Support splats inside tuple literals in type names. ([#10430](https://github.com/crystal-lang/crystal/pull/10430), thanks @HertzDevil) +- Accept pointer instance types on falsey conditional branches. ([#10464](https://github.com/crystal-lang/crystal/pull/10464), thanks @HertzDevil) +- Match named arguments by external parameter names when checking overload cover. ([#10530](https://github.com/crystal-lang/crystal/pull/10530), thanks @HertzDevil) + +### Doc generator + +- Detect source locations in more situations. ([#10439](https://github.com/crystal-lang/crystal/pull/10439), thanks @oprypin) + +## Others + +- CI improvements and housekeeping. ([#10299](https://github.com/crystal-lang/crystal/pull/10299), [#10340](https://github.com/crystal-lang/crystal/pull/10340), [#10476](https://github.com/crystal-lang/crystal/pull/10476), [#10480](https://github.com/crystal-lang/crystal/pull/10480), thanks @bcardiff, @Sija, @straight-shoota) +- Update distribution-scripts to use Shards v0.14.1. ([#10494](https://github.com/crystal-lang/crystal/pull/10494), thanks @bcardiff) +- Add GitHub issue templates. ([#8934](https://github.com/crystal-lang/crystal/pull/8934), thanks @j8r) +- Add LLVM 11.1 to the list of supported versions. ([#10523](https://github.com/crystal-lang/crystal/pull/10523), thanks @Sija) +- Fix SDL examples crashes. ([#10470](https://github.com/crystal-lang/crystal/pull/10470), thanks @megatux) + # 0.36.1 (2021-02-02) ## Standard library diff --git a/README.md b/README.md index aa0269fecf9b..3ed884e701f5 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Oh, and we don't want to write C code to make the code run faster. Project Status -------------- -Crystal is still under heavy development. There can be breaking changes but we're trying to keep them as minimum as possible. +Within a major version language features won't be removed or changed in any way that could prevent an existing code to keep compiling and working. And the built in standard library might be enriched but always with backward compatibility in mind. The development is possible thanks to the community's effort and the continued support of [84codes](https://www.84codes.com/), [Nikola Motor Company](https://nikolamotor.com/) and every other [sponsor](https://crystal-lang.org/sponsors). diff --git a/src/VERSION b/src/VERSION index 05639a55677c..3eefcb9dd5b3 100644 --- a/src/VERSION +++ b/src/VERSION @@ -1 +1 @@ -1.0.0-dev +1.0.0 From e71488174661d6805d6746cf80d41de51ecd51e4 Mon Sep 17 00:00:00 2001 From: "Brian J. Cardiff" Date: Wed, 24 Mar 2021 11:53:35 -0300 Subject: [PATCH 28/33] CI: Update to use 1.0.0 (#10533) --- .circleci/config.yml | 2 +- .github/workflows/win.yml | 2 +- bin/ci | 6 +++--- shell.nix | 12 ++++++------ src/VERSION | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 96765715089a..8f1c46c82182 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -155,7 +155,7 @@ jobs: echo "export CRYSTAL_SHA1=$CIRCLE_SHA1" >> build.env # Which previous version use - export PREVIOUS_CRYSTAL_BASE_URL="https://github.com/crystal-lang/crystal/releases/download/0.36.1/crystal-0.36.1-1" + export PREVIOUS_CRYSTAL_BASE_URL="https://github.com/crystal-lang/crystal/releases/download/1.0.0/crystal-1.0.0-1" echo "export PREVIOUS_CRYSTAL_RELEASE_LINUX64_TARGZ=${PREVIOUS_CRYSTAL_BASE_URL}-linux-x86_64.tar.gz" >> build.env echo "export PREVIOUS_CRYSTAL_RELEASE_LINUX32_TARGZ=${PREVIOUS_CRYSTAL_BASE_URL}-linux-i686.tar.gz" >> build.env echo "export PREVIOUS_CRYSTAL_RELEASE_DARWIN_TARGZ=${PREVIOUS_CRYSTAL_BASE_URL}-darwin-x86_64.tar.gz" >> build.env diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index 85fb221357b1..7175ffeabd59 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -5,7 +5,7 @@ on: [push, pull_request] jobs: linux-job: runs-on: ubuntu-latest - container: crystallang/crystal:0.36.1-build + container: crystallang/crystal:1.0.0-build steps: - name: Download Crystal source uses: actions/checkout@v2 diff --git a/bin/ci b/bin/ci index 05cca928eaa1..1c857c78f133 100755 --- a/bin/ci +++ b/bin/ci @@ -133,8 +133,8 @@ format() { prepare_build() { on_linux verify_linux_environment - on_osx curl -L https://github.com/crystal-lang/crystal/releases/download/0.36.1/crystal-0.36.1-1-darwin-x86_64.tar.gz -o ~/crystal.tar.gz - on_osx 'pushd ~;gunzip -c ~/crystal.tar.gz | tar xopf -;mv crystal-0.36.1-1 crystal;popd' + on_osx curl -L https://github.com/crystal-lang/crystal/releases/download/1.0.0/crystal-1.0.0-1-darwin-x86_64.tar.gz -o ~/crystal.tar.gz + on_osx 'pushd ~;gunzip -c ~/crystal.tar.gz | tar xopf -;mv crystal-1.0.0-1 crystal;popd' # These commands may take a few minutes to run due to the large size of the repositories. # This restriction has been made on GitHub's request because updating shallow @@ -187,7 +187,7 @@ with_build_env() { on_linux verify_linux_environment - export DOCKER_TEST_PREFIX="${DOCKER_TEST_PREFIX:=crystallang/crystal:0.36.1}" + export DOCKER_TEST_PREFIX="${DOCKER_TEST_PREFIX:=crystallang/crystal:1.0.0}" case $ARCH in x86_64) diff --git a/shell.nix b/shell.nix index cc50de036db8..f95a0f4a061a 100644 --- a/shell.nix +++ b/shell.nix @@ -52,18 +52,18 @@ let # Hashes obtained using `nix-prefetch-url --unpack ` latestCrystalBinary = genericBinary ({ x86_64-darwin = { - url = "https://github.com/crystal-lang/crystal/releases/download/0.36.1/crystal-0.36.1-1-darwin-x86_64.tar.gz"; - sha256 = "sha256:0596aind8mlg65g99zhd9r1bic8scs5j6262xnh43r3fracw00lj"; + url = "https://github.com/crystal-lang/crystal/releases/download/1.0.0/crystal-1.0.0-1-darwin-x86_64.tar.gz"; + sha256 = "sha256:1ff05f7v31r7xw4xk1a5zns77k3hrgdb9cn15w2zsps83iqlq81i"; }; x86_64-linux = { - url = "https://github.com/crystal-lang/crystal/releases/download/0.36.1/crystal-0.36.1-1-linux-x86_64.tar.gz"; - sha256 = "sha256:1vpxm0zw5nbfl0pxl3gsm622958fqlwf71q2gm73avwj8kmhwsx6"; + url = "https://github.com/crystal-lang/crystal/releases/download/1.0.0/crystal-1.0.0-1-linux-x86_64.tar.gz"; + sha256 = "sha256:13940gjs1zl29wrhngzylhckxgzb8xh16bniqik5lslp6qpljqy4"; }; i686-linux = { - url = "https://github.com/crystal-lang/crystal/releases/download/0.36.1/crystal-0.36.1-1-linux-i686.tar.gz"; - sha256 = "sha256:0a1x7w6690dhsjl9bdin1z79bx59ch5w0am1snn3bnjdpc1zbnbs"; + url = "https://github.com/crystal-lang/crystal/releases/download/1.0.0/crystal-1.0.0-1-linux-i686.tar.gz"; + sha256 = "sha256:18xg2nxg68cx0ngidpzy68wa5zqmcz0xfm0im5sg8j8bnj8ccg35"; }; }.${pkgs.stdenv.system}); diff --git a/src/VERSION b/src/VERSION index 3eefcb9dd5b3..336c36775594 100644 --- a/src/VERSION +++ b/src/VERSION @@ -1 +1 @@ -1.0.0 +1.1.0-dev From fb8bbed91e3b45dfc78dd7ccf365a8f2a24d2889 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Tue, 30 Mar 2021 17:18:40 +0800 Subject: [PATCH 29/33] Exclude variables' final types inside while true if re-assigned before first break (#10538) --- spec/compiler/semantic/while_spec.cr | 74 +++++++++++++++++++ src/compiler/crystal/semantic/main_visitor.cr | 38 ++++++++-- 2 files changed, 104 insertions(+), 8 deletions(-) diff --git a/spec/compiler/semantic/while_spec.cr b/spec/compiler/semantic/while_spec.cr index 7968300c0a97..30f902bfca69 100644 --- a/spec/compiler/semantic/while_spec.cr +++ b/spec/compiler/semantic/while_spec.cr @@ -170,6 +170,80 @@ describe "Semantic: while" do )) { nilable int32 } end + it "doesn't use type at end of endless while if variable is reassigned" do + assert_type(%( + while true + a = 1 + if 1 == 1 + break + end + a = 'x' + end + a + )) { int32 } + end + + it "doesn't use type at end of endless while if variable is reassigned (2)" do + assert_type(%( + a = "" + while true + a = 1 + if 1 == 1 + break + end + a = 'x' + end + a + )) { int32 } + end + + it "doesn't use type at end of endless while if variable is reassigned (3)" do + assert_type(%( + a = {1} + while true + a = a[0] + if 1 == 1 + break + end + a = {'x'} + end + a + )) { union_of(int32, char) } + end + + it "uses type at end of endless while if variable is reassigned, but not before first break" do + assert_type(%( + while true + if 1 == 1 + break + end + a = 1 + if 1 == 1 + break + end + a = 'x' + end + a + )) { nilable union_of(int32, char) } + end + + it "uses type at end of endless while if variable is reassigned, but not before first break (2)" do + assert_type(%( + a = "" + while true + if 1 == 1 + break + end + a = 1 + if 1 == 1 + break + end + a = 'x' + end + a + )) { union_of(int32, char, string) } + end + it "rebinds condition variable after while body (#6158)" do assert_type(%( class Foo diff --git a/src/compiler/crystal/semantic/main_visitor.cr b/src/compiler/crystal/semantic/main_visitor.cr index 3807af827c38..fab571bf4a93 100644 --- a/src/compiler/crystal/semantic/main_visitor.cr +++ b/src/compiler/crystal/semantic/main_visitor.cr @@ -2139,8 +2139,28 @@ module Crystal # If the loop is endless if endless - after_while_var.bind_to(while_var) - after_while_var.nil_if_read = while_var.nil_if_read? + # Suppose we have + # + # x = exp1 + # while true + # x = exp2 + # break if ... + # x = exp3 + # break if ... + # x = exp4 + # end + # + # Here the type of x after the loop will never be affected by + # `x = exp4`, because `x = exp2` must have been executed before the + # loop may exit at the first break. Therefore, if the x right before + # the first break is different from the last x, we don't use the + # latter's type upon exit (but exp2 itself may depend on exp4 if it + # refers to x). + break_var = all_break_vars.try &.dig?(0, name) + unless break_var && !break_var.same?(while_var) + after_while_var.bind_to(while_var) + after_while_var.nil_if_read = while_var.nil_if_read? + end else # We need to bind to the variable *before* the condition, even # after before the variables that are used in the condition @@ -2159,21 +2179,23 @@ module Crystal # outside it must be nilable, unless the loop is endless. else after_while_var = MetaVar.new(name) - after_while_var.bind_to(while_var) - nilable = false + if endless + break_var = all_break_vars.try &.dig?(0, name) + unless break_var && !break_var.same?(while_var) + after_while_var.bind_to(while_var) + end + # In an endless loop if not all variable with the given name end up # in a break it means that they can be nilable. # Alternatively, if any var that ends in a break is nil-if-read then # the resulting variable will be nil-if-read too. if !all_break_vars.try(&.all? &.has_key?(name)) || all_break_vars.try(&.any? &.[name]?.try &.nil_if_read?) - nilable = true + after_while_var.nil_if_read = true end else - nilable = true - end - if nilable + after_while_var.bind_to(while_var) after_while_var.nil_if_read = true end From b3f76ae406ebd3087573b4c78425b3d4c081696c Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Tue, 30 Mar 2021 20:19:07 +1100 Subject: [PATCH 30/33] fix undefined constant error for http/params (#10537) `Error: undefined constant ::URI::Params` --- src/http/params.cr | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/http/params.cr b/src/http/params.cr index 09a8b5fae7cd..3a8c33204504 100644 --- a/src/http/params.cr +++ b/src/http/params.cr @@ -1,3 +1,5 @@ +require "uri/params" + module HTTP alias Params = ::URI::Params end From e5667c00da1913db78360e427d8c376716b9caf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Tue, 30 Mar 2021 11:19:13 +0200 Subject: [PATCH 31/33] Add usage instructions for spec runner to compiler (#10046) --- src/compiler/crystal/command/spec.cr | 16 +++- src/spec.cr | 61 +++---------- src/spec/cli.cr | 128 +++++++++++++++++++++++++++ src/spec/dsl.cr | 73 --------------- 4 files changed, 155 insertions(+), 123 deletions(-) create mode 100644 src/spec/cli.cr diff --git a/src/compiler/crystal/command/spec.cr b/src/compiler/crystal/command/spec.cr index 6aef2d81c0fe..c54e4a00cc18 100644 --- a/src/compiler/crystal/command/spec.cr +++ b/src/compiler/crystal/command/spec.cr @@ -8,14 +8,28 @@ # directory, which usually is just `require "spec"` but could # be anything else (for example the `minitest` shard). +# Gain access to OptionParser for spec runner to include it in the usage +# instructions. +require "spec/cli" + class Crystal::Command private def spec compiler = new_compiler link_flags = [] of String OptionParser.parse(options) do |opts| - opts.banner = "Usage: crystal spec [options] [files]\n\nOptions:" + opts.banner = "Usage: crystal spec [options] [files] [runtime_options]\n\nOptions:" setup_simple_compiler_options compiler, opts + opts.on("-h", "--help", "Show this message") do + puts opts + puts + + runtime_options = Spec.option_parser + runtime_options.banner = "Runtime options (passed to spec runner):" + puts runtime_options + exit + end + opts.on("--link-flags FLAGS", "Additional flags to pass to the linker") do |some_link_flags| link_flags << some_link_flags end diff --git a/src/spec.cr b/src/spec.cr index c8290f673d0a..1df81a9b7b8c 100644 --- a/src/spec.cr +++ b/src/spec.cr @@ -1,4 +1,5 @@ require "./spec/dsl" +require "./spec/cli" # Crystal's built-in testing library. It provides a structure for writing executable examples # of how your code should behave. A domain specific language allows you to write them in a way similar to natural language. @@ -94,61 +95,23 @@ end Colorize.on_tty_only! -OptionParser.parse do |opts| - opts.banner = "crystal spec runner" - opts.on("-e ", "--example STRING", "run examples whose full nested names include STRING") do |pattern| - Spec.pattern = pattern - end - opts.on("-l ", "--line LINE", "run examples whose line matches LINE") do |line| - Spec.line = line.to_i - end - opts.on("-p", "--profile", "Print the 10 slowest specs") do - Spec.slowest = 10 - end - opts.on("--fail-fast", "abort the run on first failure") do - Spec.fail_fast = true - end - opts.on("--location file:line", "run example at line 'line' in file 'file', multiple allowed") do |location| - if location =~ /\A(.+?)\:(\d+)\Z/ - Spec.add_location $1, $2.to_i - else - STDERR.puts "location #{location} must be file:line" - exit 1 - end - end - opts.on("--tag TAG", "run examples with the specified TAG, or exclude examples by adding ~ before the TAG.") do |tag| - Spec.add_tag tag - end - opts.on("--order MODE", "run examples in random order by passing MODE as 'random' or to a specific seed by passing MODE as the seed value") do |mode| - if mode == "default" || mode == "random" - Spec.order = mode - elsif seed = mode.to_u64? - Spec.order = seed - else - abort("order must be either 'default', 'random', or a numeric seed value") - end - end - opts.on("--junit_output OUTPUT_PATH", "generate JUnit XML output within the given OUTPUT_PATH") do |output_path| - junit_formatter = Spec::JUnitFormatter.file(Path.new(output_path)) +# :nodoc: +# +# Implement formatter configuration. +def Spec.configure_formatter(formatter, output_path = nil) + case formatter + when "junit" + junit_formatter = Spec::JUnitFormatter.file(Path.new(output_path.not_nil!)) Spec.add_formatter(junit_formatter) - end - opts.on("-h", "--help", "show this help") do |pattern| - puts opts - exit - end - opts.on("-v", "--verbose", "verbose output") do + when "verbose" Spec.override_default_formatter(Spec::VerboseFormatter.new) - end - opts.on("--tap", "Generate TAP output (Test Anything Protocol)") do + when "tap" Spec.override_default_formatter(Spec::TAPFormatter.new) end - opts.on("--no-color", "Disable colored output") do - Spec.use_colors = false - end - opts.unknown_args do |args| - end end +Spec.option_parser.parse(ARGV) + unless ARGV.empty? STDERR.puts "Error: unknown argument '#{ARGV.first}'" exit 1 diff --git a/src/spec/cli.cr b/src/spec/cli.cr new file mode 100644 index 000000000000..f0d36f45348d --- /dev/null +++ b/src/spec/cli.cr @@ -0,0 +1,128 @@ +require "option_parser" + +# This file is included in the compiler to add usage instructions for the +# spec runner on `crystal spec --help`. + +module Spec + # :nodoc: + class_property? use_colors = true + + # :nodoc: + class_property pattern : Regex? + + # :nodoc: + class_property line : Int32? + + # :nodoc: + class_property slowest : Int32? + + # :nodoc: + class_property? fail_fast = false + + # :nodoc: + class_property? focus = false + + # :nodoc: + def self.add_location(file, line) + locations = @@locations ||= {} of String => Array(Int32) + lines = locations[File.expand_path(file)] ||= [] of Int32 + lines << line + end + + # :nodoc: + def self.add_tag(tag) + if anti_tag = tag.lchop?('~') + (@@anti_tags ||= Set(String).new) << anti_tag + else + (@@tags ||= Set(String).new) << tag + end + end + + # :nodoc: + class_getter randomizer_seed : UInt64? + class_getter randomizer : Random::PCG32? + + # :nodoc: + def self.order=(mode) + seed = + case mode + when "default" + nil + when "random" + Random::Secure.rand(1..99999).to_u64 # 5 digits or less for simplicity + when UInt64 + mode + else + raise ArgumentError.new("order must be either 'default', 'random', or a numeric seed value") + end + + @@randomizer_seed = seed + @@randomizer = seed ? Random::PCG32.new(seed) : nil + end + + # :nodoc: + class_property option_parser : OptionParser = begin + OptionParser.new do |opts| + opts.banner = "crystal spec runner" + opts.on("-e", "--example STRING", "run examples whose full nested names include STRING") do |pattern| + Spec.pattern = Regex.new(Regex.escape(pattern)) + end + opts.on("-l", "--line LINE", "run examples whose line matches LINE") do |line| + Spec.line = line.to_i + end + opts.on("-p", "--profile", "Print the 10 slowest specs") do + Spec.slowest = 10 + end + opts.on("--fail-fast", "abort the run on first failure") do + Spec.fail_fast = true + end + opts.on("--location file:line", "run example at line 'line' in file 'file', multiple allowed") do |location| + if location =~ /\A(.+?)\:(\d+)\Z/ + Spec.add_location $1, $2.to_i + else + STDERR.puts "location #{location} must be file:line" + exit 1 + end + end + opts.on("--tag TAG", "run examples with the specified TAG, or exclude examples by adding ~ before the TAG.") do |tag| + Spec.add_tag tag + end + opts.on("--order MODE", "run examples in random order by passing MODE as 'random' or to a specific seed by passing MODE as the seed value") do |mode| + if mode == "default" || mode == "random" + Spec.order = mode + elsif seed = mode.to_u64? + Spec.order = seed + else + abort("order must be either 'default', 'random', or a numeric seed value") + end + end + opts.on("--junit_output OUTPUT_PATH", "generate JUnit XML output within the given OUTPUT_PATH") do |output_path| + configure_formatter("junit", output_path) + end + opts.on("-h", "--help", "show this help") do |pattern| + puts opts + exit + end + opts.on("-v", "--verbose", "verbose output") do + configure_formatter("verbose") + end + opts.on("--tap", "Generate TAP output (Test Anything Protocol)") do + configure_formatter("tap") + end + opts.on("--no-color", "Disable colored output") do + Spec.use_colors = false + end + opts.unknown_args do |args| + end + end + end + + # :nodoc: + # + # Blank implementation to reduce the interface of spec's option parser for + # inclusion in the compiler. This avoids depending on more of `Spec` + # module. + # The real implementation in `../spec.cr` overrides this for actual use. + def self.configure_formatter(formatter, output_path = nil) + end +end diff --git a/src/spec/dsl.cr b/src/spec/dsl.cr index 357f9de95d54..084e1a07c7b4 100644 --- a/src/spec/dsl.cr +++ b/src/spec/dsl.cr @@ -19,8 +19,6 @@ module Spec pending: '*', } - @@use_colors = true - # :nodoc: def self.color(str, status) if use_colors? @@ -30,15 +28,6 @@ module Spec end end - # :nodoc: - def self.use_colors? - @@use_colors - end - - # :nodoc: - def self.use_colors=(@@use_colors) - end - # :nodoc: class SpecError < Exception getter file : String @@ -69,46 +58,6 @@ module Spec finish_run end - # :nodoc: - class_getter randomizer_seed : UInt64? - class_getter randomizer : Random::PCG32? - - # :nodoc: - def self.order=(mode) - seed = - case mode - when "default" - nil - when "random" - Random::Secure.rand(1..99999).to_u64 # 5 digits or less for simplicity - when UInt64 - mode - else - raise ArgumentError.new("order must be either 'default', 'random', or a numeric seed value") - end - - @@randomizer_seed = seed - @@randomizer = seed ? Random::PCG32.new(seed) : nil - end - - # :nodoc: - def self.pattern=(pattern) - @@pattern = Regex.new(Regex.escape(pattern)) - end - - # :nodoc: - def self.line=(@@line : Int32) - end - - # :nodoc: - def self.slowest=(@@slowest : Int32) - end - - # :nodoc: - def self.slowest - @@slowest - end - # :nodoc: def self.to_human(span : Time::Span) total_milliseconds = span.total_milliseconds @@ -130,22 +79,6 @@ module Spec "#{minutes}:#{seconds < 10 ? "0" : ""}#{seconds} minutes" end - # :nodoc: - def self.add_location(file, line) - locations = @@locations ||= {} of String => Array(Int32) - lines = locations[File.expand_path(file)] ||= [] of Int32 - lines << line - end - - # :nodoc: - def self.add_tag(tag) - if anti_tag = tag.lchop?('~') - (@@anti_tags ||= Set(String).new) << anti_tag - else - (@@tags ||= Set(String).new) << tag - end - end - record SplitFilter, remainder : Int32, quotient : Int32 @@split_filter : SplitFilter? = nil @@ -159,12 +92,6 @@ module Spec end end - # :nodoc: - class_property? fail_fast = false - - # :nodoc: - class_property? focus = false - # Instructs the spec runner to execute the given block # before each spec in the spec suite. # From a0ce5f38852f8ca95a73ee124338cb27f5c680aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Tue, 30 Mar 2021 11:19:59 +0200 Subject: [PATCH 32/33] Add OAuth2::Client#http_client (#10452) --- spec/std/http/spec_helper.cr | 33 +++++++++++++ spec/std/oauth2/client_spec.cr | 85 +++++++++++++++++++--------------- src/oauth2/client.cr | 13 +++++- 3 files changed, 93 insertions(+), 38 deletions(-) diff --git a/spec/std/http/spec_helper.cr b/spec/std/http/spec_helper.cr index e3c23a5ff448..d5fd8a72e454 100644 --- a/spec/std/http/spec_helper.cr +++ b/spec/std/http/spec_helper.cr @@ -47,3 +47,36 @@ def run_server(server) end end end + +# Helper method which runs a *handler* +# Similar to `run_server` but doesn't go through the network stack. +def run_handler(handler) + done = Channel(Exception?).new + + begin + IO::Stapled.pipe do |server_io, client_io| + processor = HTTP::Server::RequestProcessor.new(handler) + f = spawn do + processor.process(server_io, server_io) + rescue exc + done.send exc + else + done.send nil + end + + client = HTTP::Client.new(client_io) + + begin + wait_until_blocked f + + yield client + ensure + processor.close + server_io.close + if exc = done.receive + raise exc + end + end + end + end +end diff --git a/spec/std/oauth2/client_spec.cr b/spec/std/oauth2/client_spec.cr index e1224574efbe..447103b88911 100644 --- a/spec/std/oauth2/client_spec.cr +++ b/spec/std/oauth2/client_spec.cr @@ -42,16 +42,34 @@ describe OAuth2::Client do describe "get_access_token_using_*" do describe "using HTTP Basic authentication to pass credentials" do it "#get_access_token_using_authorization_code" do - server = HTTP::Server.new do |context| + handler = HTTP::Handler::HandlerProc.new do |context| body = context.request.body.not_nil!.gets_to_end response = {access_token: "access_token", body: body} context.response.print response.to_json end - address = server.bind_unused_port "127.0.0.1" + run_handler(handler) do |http_client| + client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", scheme: "http" + client.http_client = http_client + + token = client.get_access_token_using_authorization_code(authorization_code: "SDFhw39fwfg23flSfpawbef") + token.extra.not_nil!["body"].should eq %("redirect_uri=&grant_type=authorization_code&code=SDFhw39fwfg23flSfpawbef") + token.access_token.should eq "access_token" + end + end + + it "configures HTTP::Client" do + server = HTTP::Server.new do |context| + body = context.request.body.not_nil!.gets_to_end + response = {access_token: "access_token", body: body} + context.response.print response.to_json + end + address = server.bind_tcp 0 run_server(server) do client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", port: address.port, scheme: "http" + client.http_client.port.should eq address.port + client.http_client.host.should eq "127.0.0.1" token = client.get_access_token_using_authorization_code(authorization_code: "SDFhw39fwfg23flSfpawbef") token.extra.not_nil!["body"].should eq %("redirect_uri=&grant_type=authorization_code&code=SDFhw39fwfg23flSfpawbef") @@ -60,16 +78,15 @@ describe OAuth2::Client do end it "#get_access_token_using_resource_owner_credentials" do - server = HTTP::Server.new do |context| + handler = HTTP::Handler::HandlerProc.new do |context| body = context.request.body.not_nil!.gets_to_end response = {access_token: "access_token", body: body} context.response.print response.to_json end - address = server.bind_unused_port "127.0.0.1" - - run_server(server) do - client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", port: address.port, scheme: "http" + run_handler(handler) do |http_client| + client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", scheme: "http" + client.http_client = http_client token = client.get_access_token_using_resource_owner_credentials(username: "user123", password: "monkey", scope: "read_posts") token.extra.not_nil!["body"].should eq %("grant_type=password&username=user123&password=monkey&scope=read_posts") @@ -78,16 +95,15 @@ describe OAuth2::Client do end it "#get_access_token_using_client_credentials" do - server = HTTP::Server.new do |context| + handler = HTTP::Handler::HandlerProc.new do |context| body = context.request.body.not_nil!.gets_to_end response = {access_token: "access_token", body: body} context.response.print response.to_json end - address = server.bind_unused_port "127.0.0.1" - - run_server(server) do - client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", port: address.port, scheme: "http" + run_handler(handler) do |http_client| + client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", scheme: "http" + client.http_client = http_client token = client.get_access_token_using_client_credentials(scope: "read_posts") token.extra.not_nil!["body"].should eq %("grant_type=client_credentials&scope=read_posts") @@ -96,16 +112,15 @@ describe OAuth2::Client do end it "#get_access_token_using_refresh_token" do - server = HTTP::Server.new do |context| + handler = HTTP::Handler::HandlerProc.new do |context| body = context.request.body.not_nil!.gets_to_end response = {access_token: "access_token", body: body} context.response.print response.to_json end - address = server.bind_unused_port "127.0.0.1" - - run_server(server) do - client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", port: address.port, scheme: "http" + run_handler(handler) do |http_client| + client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", scheme: "http" + client.http_client = http_client token = client.get_access_token_using_refresh_token(scope: "read_posts", refresh_token: "some_refresh_token") token.extra.not_nil!["body"].should eq %("grant_type=refresh_token&refresh_token=some_refresh_token&scope=read_posts") @@ -115,16 +130,15 @@ describe OAuth2::Client do end describe "using Request Body to pass credentials" do it "#get_access_token_using_authorization_code" do - server = HTTP::Server.new do |context| + handler = HTTP::Handler::HandlerProc.new do |context| body = context.request.body.not_nil!.gets_to_end response = {access_token: "access_token", body: body} context.response.print response.to_json end - address = server.bind_unused_port "127.0.0.1" - - run_server(server) do - client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", port: address.port, scheme: "http", auth_scheme: OAuth2::AuthScheme::RequestBody + run_handler(handler) do |http_client| + client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", scheme: "http", auth_scheme: OAuth2::AuthScheme::RequestBody + client.http_client = http_client token = client.get_access_token_using_authorization_code(authorization_code: "SDFhw39fwfg23flSfpawbef") token.extra.not_nil!["body"].should eq %("client_id=client_id&client_secret=client_secret&redirect_uri=&grant_type=authorization_code&code=SDFhw39fwfg23flSfpawbef") @@ -133,16 +147,15 @@ describe OAuth2::Client do end it "#get_access_token_using_resource_owner_credentials" do - server = HTTP::Server.new do |context| + handler = HTTP::Handler::HandlerProc.new do |context| body = context.request.body.not_nil!.gets_to_end response = {access_token: "access_token", body: body} context.response.print response.to_json end - address = server.bind_unused_port "127.0.0.1" - - run_server(server) do - client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", port: address.port, scheme: "http", auth_scheme: OAuth2::AuthScheme::RequestBody + run_handler(handler) do |http_client| + client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", scheme: "http", auth_scheme: OAuth2::AuthScheme::RequestBody + client.http_client = http_client token = client.get_access_token_using_resource_owner_credentials(username: "user123", password: "monkey", scope: "read_posts") token.extra.not_nil!["body"].should eq %("client_id=client_id&client_secret=client_secret&grant_type=password&username=user123&password=monkey&scope=read_posts") @@ -151,16 +164,15 @@ describe OAuth2::Client do end it "#get_access_token_using_client_credentials" do - server = HTTP::Server.new do |context| + handler = HTTP::Handler::HandlerProc.new do |context| body = context.request.body.not_nil!.gets_to_end response = {access_token: "access_token", body: body} context.response.print response.to_json end - address = server.bind_unused_port "127.0.0.1" - - run_server(server) do - client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", port: address.port, scheme: "http", auth_scheme: OAuth2::AuthScheme::RequestBody + run_handler(handler) do |http_client| + client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", scheme: "http", auth_scheme: OAuth2::AuthScheme::RequestBody + client.http_client = http_client token = client.get_access_token_using_client_credentials(scope: "read_posts") token.extra.not_nil!["body"].should eq %("client_id=client_id&client_secret=client_secret&grant_type=client_credentials&scope=read_posts") @@ -169,16 +181,15 @@ describe OAuth2::Client do end it "#get_access_token_using_refresh_token" do - server = HTTP::Server.new do |context| + handler = HTTP::Handler::HandlerProc.new do |context| body = context.request.body.not_nil!.gets_to_end response = {access_token: "access_token", body: body} context.response.print response.to_json end - address = server.bind_unused_port "127.0.0.1" - - run_server(server) do - client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", port: address.port, scheme: "http", auth_scheme: OAuth2::AuthScheme::RequestBody + run_handler(handler) do |http_client| + client = OAuth2::Client.new "127.0.0.1", "client_id", "client_secret", scheme: "http", auth_scheme: OAuth2::AuthScheme::RequestBody + client.http_client = http_client token = client.get_access_token_using_refresh_token(scope: "read_posts", refresh_token: "some_refresh_token") token.extra.not_nil!["body"].should eq %("client_id=client_id&client_secret=client_secret&grant_type=refresh_token&refresh_token=some_refresh_token&scope=read_posts") diff --git a/src/oauth2/client.cr b/src/oauth2/client.cr index 28ef59cd30be..e2709ec04053 100644 --- a/src/oauth2/client.cr +++ b/src/oauth2/client.cr @@ -54,6 +54,17 @@ # You can also use an `OAuth2::Session` to automatically refresh expired # tokens before each request. class OAuth2::Client + # Sets the `HTTP::Client` to use with this client. + setter http_client : HTTP::Client? + + # Returns the `HTTP::Client` to use with this client. + # + # By default, this returns a new instance every time. To reuse the same instance, + # one can be assigned with `#http_client=`. + def http_client : HTTP::Client + @http_client || HTTP::Client.new(token_uri) + end + # Creates an OAuth client. # # Any or all of the customizable URIs *authorize_uri* and @@ -168,7 +179,7 @@ class OAuth2::Client yield form end - response = HTTP::Client.post token_uri, form: body, headers: headers + response = http_client.post token_uri.request_target, form: body, headers: headers case response.status when .ok?, .created? OAuth2::AccessToken.from_json(response.body) From e7b46c40739902db70a2dceebd09caf1a7c70553 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Tue, 30 Mar 2021 17:21:14 +0800 Subject: [PATCH 33/33] Fix logic for subclass restricted against uninstantiated nested generic superclass (#10522) --- spec/compiler/codegen/is_a_spec.cr | 72 +++++++++++++++++++ spec/compiler/semantic/restrictions_spec.cr | 48 +++++++++++++ src/compiler/crystal/semantic/restrictions.cr | 35 +++++++-- 3 files changed, 148 insertions(+), 7 deletions(-) diff --git a/spec/compiler/codegen/is_a_spec.cr b/spec/compiler/codegen/is_a_spec.cr index 77289e3902e6..46250bc81400 100644 --- a/spec/compiler/codegen/is_a_spec.cr +++ b/spec/compiler/codegen/is_a_spec.cr @@ -687,6 +687,78 @@ describe "Codegen: is_a?" do )).to_i.should eq(2) end + it "does is_a?(generic type) for nested generic inheritance (1) (#9660)" do + run(%( + class Cxx + end + + class Foo(T) + end + + class Bar(T) < Foo(T) + end + + class Baz < Bar(Cxx) + end + + Baz.new.is_a?(Foo) + ), inject_primitives: false).to_b.should be_true + end + + it "does is_a?(generic type) for nested generic inheritance (2)" do + run(%( + class Cxx + end + + class Foo(T) + end + + class Bar(T) < Foo(T) + end + + class Baz(T) < Bar(T) + end + + Baz(Cxx).new.is_a?(Foo) + ), inject_primitives: false).to_b.should be_true + end + + it "does is_a?(generic type) for nested generic inheritance, through upcast (1)" do + run(%( + class Cxx + end + + class Foo(T) + end + + class Bar(T) < Foo(T) + end + + class Baz < Bar(Cxx) + end + + Baz.new.as(Foo(Cxx)).is_a?(Bar) + ), inject_primitives: false).to_b.should be_true + end + + it "does is_a?(generic type) for nested generic inheritance, through upcast (2)" do + run(%( + class Cxx + end + + class Foo(T) + end + + class Bar(T) < Foo(T) + end + + class Baz(T) < Bar(T) + end + + Baz(Cxx).new.as(Foo(Cxx)).is_a?(Bar) + ), inject_primitives: false).to_b.should be_true + end + it "doesn't consider generic type to be a generic type of a recursive alias (#3524)" do run(%( class Gen(T) diff --git a/spec/compiler/semantic/restrictions_spec.cr b/spec/compiler/semantic/restrictions_spec.cr index 08107002e1dd..3d9567972df8 100644 --- a/spec/compiler/semantic/restrictions_spec.cr +++ b/spec/compiler/semantic/restrictions_spec.cr @@ -60,6 +60,54 @@ describe "Restrictions" do mod.t("Axx+").restrict(mod.t("Mxx"), MatchContext.new(mod, mod)).should eq(mod.union_of(mod.t("Bxx+"), mod.t("Cxx+"))) end + + it "restricts class against uninstantiated generic base class through multiple inheritance (1) (#9660)" do + mod = Program.new + mod.semantic parse(" + class Axx(T); end + class Bxx(T) < Axx(T); end + class Cxx < Bxx(Int32); end + ") + + result = mod.t("Cxx").restrict(mod.t("Axx"), MatchContext.new(mod, mod)) + result.should eq(mod.t("Cxx")) + end + + it "restricts class against uninstantiated generic base class through multiple inheritance (2) (#9660)" do + mod = Program.new + mod.semantic parse(" + class Axx(T); end + class Bxx(T) < Axx(T); end + class Cxx(T) < Bxx(T); end + ") + + result = mod.generic_class("Cxx", mod.int32).restrict(mod.t("Axx"), MatchContext.new(mod, mod)) + result.should eq(mod.generic_class("Cxx", mod.int32)) + end + + it "restricts virtual generic class against uninstantiated generic subclass (1)" do + mod = Program.new + mod.semantic parse(" + class Axx(T); end + class Bxx(T) < Axx(T); end + class Cxx < Bxx(Int32); end + ") + + result = mod.generic_class("Axx", mod.int32).virtual_type.restrict(mod.generic_class("Bxx", mod.int32), MatchContext.new(mod, mod)) + result.should eq(mod.generic_class("Bxx", mod.int32).virtual_type) + end + + it "restricts virtual generic class against uninstantiated generic subclass (2)" do + mod = Program.new + mod.semantic parse(" + class Axx(T); end + class Bxx(T) < Axx(T); end + class Cxx(T) < Bxx(T); end + ") + + result = mod.generic_class("Axx", mod.int32).virtual_type.restrict(mod.generic_class("Bxx", mod.int32), MatchContext.new(mod, mod)) + result.should eq(mod.generic_class("Bxx", mod.int32).virtual_type) + end end describe "restriction_of?" do diff --git a/src/compiler/crystal/semantic/restrictions.cr b/src/compiler/crystal/semantic/restrictions.cr index 8fc128422667..74811e7c8356 100644 --- a/src/compiler/crystal/semantic/restrictions.cr +++ b/src/compiler/crystal/semantic/restrictions.cr @@ -460,6 +460,19 @@ module Crystal implements?(other.base_type) ? self : nil end + def restrict(other : GenericClassType, context) + parents.try &.each do |parent| + if parent.module? + return self if parent.restriction_of?(other, context.instantiated_type, context) + else + restricted = parent.restrict other, context + return self if restricted + end + end + + nil + end + def restrict(other : Union, context) # Match all concrete types first free_var_count = other.types.count do |other_type| @@ -728,7 +741,18 @@ module Crystal end def restrict(other : GenericType, context) - generic_type == other ? self : super + return self if generic_type == other + + parents.try &.each do |parent| + if parent.module? + return self if parent.restriction_of?(other, context.instantiated_type, context) + else + restricted = parent.restrict other, context + return self if restricted + end + end + + nil end def restrict(other : Generic, context) @@ -1016,14 +1040,11 @@ module Crystal elsif base_type.is_a?(GenericInstanceType) && other.is_a?(GenericType) # Consider the case of Foo(Int32) vs. Bar(T), with Bar(T) < Foo(T): # we want to return Bar(Int32), so we search in Bar's generic instantiations - other.each_instantiated_type do |instance| + types = other.instantiated_types.compact_map do |instance| next if instance.unbound? || instance.abstract? - - if instance.implements?(base_type) - return instance - end + instance.virtual_type if instance.implements?(base_type) end - nil + program.type_merge_union_of types else nil end