Split Arguments class by functionality

Code changes for https://github.com/icy-arctic-fox/spectator/issues/47 caused a drastic increase in compilation times.
This improves compilation times by splitting concerns for arguments.
In one case, arguments are used for matching.
In the other, arguments are captured for comparison.
The second case has been moved to a FormalArguments class.
Theoretically, this reduces the complexity and combinations the compiler might be iterating.
This commit is contained in:
Michael Miller 2022-11-27 22:26:19 -07:00
parent 015d36ea4c
commit 8efd38fbdd
No known key found for this signature in database
GPG key ID: 32B47AE8F388A1FF
7 changed files with 682 additions and 267 deletions

View file

@ -1,13 +1,11 @@
require "../../spec_helper" require "../../spec_helper"
Spectator.describe Spectator::Arguments do Spectator.describe Spectator::Arguments do
subject(arguments) { Spectator::Arguments.new({42, "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } subject(arguments) { Spectator::Arguments.new({42, "foo"}, {bar: "baz", qux: 123}) }
it "stores the arguments" do it "stores the arguments" do
expect(arguments).to have_attributes( expect(arguments).to have_attributes(
args: {42, "foo"}, args: {42, "foo"},
splat_name: :splat,
splat: {:x, :y, :z},
kwargs: {bar: "baz", qux: 123} kwargs: {bar: "baz", qux: 123}
) )
end end
@ -27,33 +25,6 @@ Spectator.describe Spectator::Arguments do
expect(arguments[1]).to eq("foo") expect(arguments[1]).to eq("foo")
end end
end end
it "returns splat arguments" do
aggregate_failures do
expect(arguments[2]).to eq(:x)
expect(arguments[3]).to eq(:y)
expect(arguments[4]).to eq(:z)
end
end
context "with named positional arguments" do
subject(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
it "returns a positional argument" do
aggregate_failures do
expect(arguments[0]).to eq(42)
expect(arguments[1]).to eq("foo")
end
end
it "returns splat arguments" do
aggregate_failures do
expect(arguments[2]).to eq(:x)
expect(arguments[3]).to eq(:y)
expect(arguments[4]).to eq(:z)
end
end
end
end end
describe "#[](symbol)" do describe "#[](symbol)" do
@ -63,31 +34,13 @@ Spectator.describe Spectator::Arguments do
expect(arguments[:qux]).to eq(123) expect(arguments[:qux]).to eq(123)
end end
end end
context "with named positional arguments" do
subject(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
it "returns a positional argument" do
aggregate_failures do
expect(arguments[:arg1]).to eq(42)
expect(arguments[:arg2]).to eq("foo")
end
end
it "returns a keyword argument" do
aggregate_failures do
expect(arguments[:bar]).to eq("baz")
expect(arguments[:qux]).to eq(123)
end
end
end
end end
describe "#to_s" do describe "#to_s" do
subject { arguments.to_s } subject { arguments.to_s }
it "formats the arguments" do it "formats the arguments" do
is_expected.to eq("(42, \"foo\", *splat: {:x, :y, :z}, bar: \"baz\", qux: 123)") is_expected.to eq("(42, \"foo\", bar: \"baz\", qux: 123)")
end end
context "when empty" do context "when empty" do
@ -97,19 +50,12 @@ Spectator.describe Spectator::Arguments do
is_expected.to eq("(no args)") is_expected.to eq("(no args)")
end end
end end
context "with a splat and no arguments" do
let(arguments) { Spectator::Arguments.build(NamedTuple.new, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
it "omits the splat name" do
is_expected.to eq("(:x, :y, :z, bar: \"baz\", qux: 123)")
end
end
end end
describe "#==" do describe "#==" do
subject { arguments == other } subject { arguments == other }
context "with Arguments" do
context "with equal arguments" do context "with equal arguments" do
let(other) { arguments } let(other) { arguments }
@ -117,59 +63,79 @@ Spectator.describe Spectator::Arguments do
end end
context "with different arguments" do context "with different arguments" do
let(other) { Spectator::Arguments.new({123, :foo, "bar"}, :splat, {1, 2, 3}, {opt: "foobar"}) } let(other) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) }
it { is_expected.to be_false } it { is_expected.to be_false }
end end
context "with the same kwargs in a different order" do context "with the same kwargs in a different order" do
let(other) { Spectator::Arguments.new(arguments.args, arguments.splat_name, arguments.splat, {qux: 123, bar: "baz"}) } let(other) { Spectator::Arguments.new(arguments.args, {qux: 123, bar: "baz"}) }
it { is_expected.to be_true } it { is_expected.to be_true }
end end
context "with a missing kwarg" do context "with a missing kwarg" do
let(other) { Spectator::Arguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: "baz"}) } let(other) { Spectator::Arguments.new(arguments.args, {bar: "baz"}) }
it { is_expected.to be_false } it { is_expected.to be_false }
end end
context "with an extra kwarg" do context "with an extra kwarg" do
let(other) { Spectator::Arguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: "baz", qux: 123, extra: 0}) } let(other) { Spectator::Arguments.new(arguments.args, {bar: "baz", qux: 123, extra: 0}) }
it { is_expected.to be_false }
end
end
context "with FormalArguments" do
context "with equal arguments" do
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) }
it { is_expected.to be_true }
end
context "with different arguments" do
let(other) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, {opt: "foobar"}) }
it { is_expected.to be_false }
end
context "with the same kwargs in a different order" do
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {qux: 123, bar: "baz"}) }
it { is_expected.to be_true }
end
context "with a missing kwarg" do
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz"}) }
it { is_expected.to be_false }
end
context "with an extra kwarg" do
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123, extra: 0}) }
it { is_expected.to be_false } it { is_expected.to be_false }
end end
context "with different splat arguments" do context "with different splat arguments" do
let(other) { Spectator::Arguments.new(arguments.args, arguments.splat_name, {1, 2, 3}, arguments.kwargs) } let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, {bar: "baz", qux: 123}) }
it { is_expected.to be_false } it { is_expected.to be_false }
end end
context "with mixed positional tuple types" do context "with mixed positional tuple types" do
let(other) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, arguments.splat_name, arguments.splat, arguments.kwargs) } let(other) { Spectator::FormalArguments.new({arg1: 42}, :splat, {"foo"}, {bar: "baz", qux: 123}) }
it { is_expected.to be_true } it { is_expected.to be_true }
end end
context "with mixed positional tuple types (flipped)" do
let(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
let(other) { Spectator::Arguments.new({42, "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
it { is_expected.to be_true }
end
context "with args spilling over into splat and mixed positional tuple types" do
let(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
let(other) { Spectator::Arguments.new({42, "foo", :x, :y, :z}, nil, nil, {bar: "baz", qux: 123}) }
it { is_expected.to be_true }
end end
end end
describe "#===" do describe "#===" do
subject { pattern === arguments } subject { pattern === arguments }
context "with Arguments" do
context "with equal arguments" do context "with equal arguments" do
let(pattern) { arguments } let(pattern) { arguments }
@ -177,92 +143,142 @@ Spectator.describe Spectator::Arguments do
end end
context "with matching arguments" do context "with matching arguments" do
let(pattern) { Spectator::Arguments.new({Int32, /foo/}, :splat, {Symbol, Symbol, :z}, {bar: /baz/, qux: Int32}) } let(pattern) { Spectator::Arguments.new({Int32, /foo/}, {bar: /baz/, qux: Int32}) }
it { is_expected.to be_true } it { is_expected.to be_true }
end end
context "with non-matching arguments" do context "with non-matching arguments" do
let(pattern) { Spectator::Arguments.new({Float64, /bar/}, :splat, {String, Int32, :x}, {bar: /foo/, qux: "123"}) } let(pattern) { Spectator::Arguments.new({Float64, /bar/}, {bar: /foo/, qux: "123"}) }
it { is_expected.to be_false } it { is_expected.to be_false }
end end
context "with different arguments" do context "with different arguments" do
let(pattern) { Spectator::Arguments.new({123, :foo, "bar"}, :splat, {1, 2, 3}, {opt: "foobar"}) } let(pattern) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) }
it { is_expected.to be_false } it { is_expected.to be_false }
end end
context "with the same kwargs in a different order" do context "with the same kwargs in a different order" do
let(pattern) { Spectator::Arguments.new(arguments.args, arguments.splat_name, arguments.splat, {qux: Int32, bar: /baz/}) } let(pattern) { Spectator::Arguments.new(arguments.args, {qux: Int32, bar: /baz/}) }
it { is_expected.to be_true } it { is_expected.to be_true }
end end
context "with an additional kwarg" do context "with an additional kwarg" do
let(pattern) { Spectator::Arguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: /baz/}) } let(pattern) { Spectator::Arguments.new(arguments.args, {bar: /baz/}) }
it { is_expected.to be_true } it { is_expected.to be_true }
end end
context "with a missing kwarg" do context "with a missing kwarg" do
let(pattern) { Spectator::Arguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: /baz/, qux: Int32, extra: 0}) } let(pattern) { Spectator::Arguments.new(arguments.args, {bar: /baz/, qux: Int32, extra: 0}) }
it { is_expected.to be_false }
end
end
context "with FormalArguments" do
let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) }
context "with equal arguments" do
let(pattern) { Spectator::Arguments.new({42, "foo"}, {bar: "baz", qux: 123}) }
it { is_expected.to be_true }
end
context "with matching arguments" do
let(pattern) { Spectator::Arguments.new({Int32, /foo/}, {bar: /baz/, qux: Int32}) }
it { is_expected.to be_true }
end
context "with non-matching arguments" do
let(pattern) { Spectator::Arguments.new({Float64, /bar/}, {bar: /foo/, qux: "123"}) }
it { is_expected.to be_false }
end
context "with different arguments" do
let(pattern) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) }
it { is_expected.to be_false }
end
context "with the same kwargs in a different order" do
let(pattern) { Spectator::Arguments.new(arguments.positional, {qux: Int32, bar: /baz/}) }
it { is_expected.to be_true }
end
context "with an additional kwarg" do
let(pattern) { Spectator::Arguments.new(arguments.positional, {bar: /baz/}) }
it { is_expected.to be_true }
end
context "with a missing kwarg" do
let(pattern) { Spectator::Arguments.new(arguments.positional, {bar: /baz/, qux: Int32, extra: 0}) }
it { is_expected.to be_false } it { is_expected.to be_false }
end end
context "with different splat arguments" do context "with different splat arguments" do
let(pattern) { Spectator::Arguments.new(arguments.args, arguments.splat_name, {1, 2, 3}, arguments.kwargs) } let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, super.kwargs) }
let(pattern) { Spectator::Arguments.new({Int32, /foo/, 5}, arguments.kwargs) }
it { is_expected.to be_false } it { is_expected.to be_false }
end end
context "with matching mixed positional tuple types" do context "with matching mixed positional tuple types" do
let(pattern) { Spectator::Arguments.new({arg1: Int32, arg2: /foo/}, arguments.splat_name, arguments.splat, arguments.kwargs) } let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, super.kwargs) }
let(pattern) { Spectator::Arguments.new({Int32, /foo/, 1, 2, 3}, arguments.kwargs) }
it { is_expected.to be_true } it { is_expected.to be_true }
end end
context "with non-matching mixed positional tuple types" do context "with non-matching mixed positional tuple types" do
let(pattern) { Spectator::Arguments.new({arg1: Float64, arg2: /bar/}, arguments.splat_name, arguments.splat, arguments.kwargs) } let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, super.kwargs) }
let(pattern) { Spectator::Arguments.new({Float64, /bar/, 3, 2, Symbol}, arguments.kwargs) }
it { is_expected.to be_false } it { is_expected.to be_false }
end end
context "with matching args spilling over into splat and mixed positional tuple types" do context "with matching args spilling over into splat and mixed positional tuple types" do
let(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
let(pattern) { Spectator::Arguments.capture(Int32, /foo/, Symbol, Symbol, :z, bar: /baz/, qux: Int32) } let(pattern) { Spectator::Arguments.capture(Int32, /foo/, Symbol, Symbol, :z, bar: /baz/, qux: Int32) }
it { is_expected.to be_true } it { is_expected.to be_true }
end end
context "with non-matching args spilling over into splat and mixed positional tuple types" do context "with non-matching args spilling over into splat and mixed positional tuple types" do
let(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
let(pattern) { Spectator::Arguments.capture(Float64, /bar/, Symbol, String, :z, bar: /foo/, qux: Int32) } let(pattern) { Spectator::Arguments.capture(Float64, /bar/, Symbol, String, :z, bar: /foo/, qux: Int32) }
it { is_expected.to be_false } it { is_expected.to be_false }
end end
context "with matching mixed named positional and keyword arguments" do context "with matching mixed named positional and keyword arguments" do
let(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
let(pattern) { Spectator::Arguments.capture(/foo/, Symbol, :y, Symbol, arg1: Int32, bar: /baz/, qux: 123) } let(pattern) { Spectator::Arguments.capture(/foo/, Symbol, :y, Symbol, arg1: Int32, bar: /baz/, qux: 123) }
it { is_expected.to be_true } it { is_expected.to be_true }
end end
context "with non-matching mixed named positional and keyword arguments" do context "with non-matching mixed named positional and keyword arguments" do
let(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
let(pattern) { Spectator::Arguments.capture(5, Symbol, :z, Symbol, arg2: /foo/, bar: /baz/, qux: Int32) } let(pattern) { Spectator::Arguments.capture(5, Symbol, :z, Symbol, arg2: /foo/, bar: /baz/, qux: Int32) }
it { is_expected.to be_false } it { is_expected.to be_false }
end end
context "with non-matching mixed named positional and keyword arguments" do context "with non-matching mixed named positional and keyword arguments" do
let(arguments) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) } let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
let(pattern) { Spectator::Arguments.capture(/bar/, String, :y, Symbol, arg1: 0, bar: /foo/, qux: Float64) } let(pattern) { Spectator::Arguments.capture(/bar/, String, :y, Symbol, arg1: 0, bar: /foo/, qux: Float64) }
it { is_expected.to be_false } it { is_expected.to be_false }
end end
end end
end end
end

View file

@ -0,0 +1,325 @@
require "../../spec_helper"
Spectator.describe Spectator::FormalArguments do
subject(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
it "stores the arguments" do
expect(arguments).to have_attributes(
args: {arg1: 42, arg2: "foo"},
splat_name: :splat,
splat: {:x, :y, :z},
kwargs: {bar: "baz", qux: 123}
)
end
describe ".build" do
subject { Spectator::FormalArguments.build({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, {bar: "baz", qux: 123}) }
it "stores the arguments and keyword arguments" do
is_expected.to have_attributes(
args: {arg1: 42, arg2: "foo"},
splat_name: :splat,
splat: {1, 2, 3},
kwargs: {bar: "baz", qux: 123}
)
end
context "without a splat" do
subject { Spectator::FormalArguments.build({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) }
it "stores the arguments and keyword arguments" do
is_expected.to have_attributes(
args: {arg1: 42, arg2: "foo"},
splat: nil,
kwargs: {bar: "baz", qux: 123}
)
end
end
end
describe "#[](index)" do
it "returns a positional argument" do
aggregate_failures do
expect(arguments[0]).to eq(42)
expect(arguments[1]).to eq("foo")
end
end
it "returns splat arguments" do
aggregate_failures do
expect(arguments[2]).to eq(:x)
expect(arguments[3]).to eq(:y)
expect(arguments[4]).to eq(:z)
end
end
context "with named positional arguments" do
subject(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
it "returns a positional argument" do
aggregate_failures do
expect(arguments[0]).to eq(42)
expect(arguments[1]).to eq("foo")
end
end
it "returns splat arguments" do
aggregate_failures do
expect(arguments[2]).to eq(:x)
expect(arguments[3]).to eq(:y)
expect(arguments[4]).to eq(:z)
end
end
end
end
describe "#[](symbol)" do
it "returns a keyword argument" do
aggregate_failures do
expect(arguments[:bar]).to eq("baz")
expect(arguments[:qux]).to eq(123)
end
end
context "with named positional arguments" do
subject(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
it "returns a positional argument" do
aggregate_failures do
expect(arguments[:arg1]).to eq(42)
expect(arguments[:arg2]).to eq("foo")
end
end
it "returns a keyword argument" do
aggregate_failures do
expect(arguments[:bar]).to eq("baz")
expect(arguments[:qux]).to eq(123)
end
end
end
end
describe "#to_s" do
subject { arguments.to_s }
it "formats the arguments" do
is_expected.to eq("(arg1: 42, arg2: \"foo\", *splat: {:x, :y, :z}, bar: \"baz\", qux: 123)")
end
context "when empty" do
let(arguments) { Spectator::FormalArguments.none }
it "returns (no args)" do
is_expected.to eq("(no args)")
end
end
context "with a splat and no arguments" do
let(arguments) { Spectator::FormalArguments.build(NamedTuple.new, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
it "omits the splat name" do
is_expected.to eq("(:x, :y, :z, bar: \"baz\", qux: 123)")
end
end
end
describe "#==" do
subject { arguments == other }
context "with Arguments" do
context "with equal arguments" do
let(other) { Spectator::Arguments.new(arguments.positional, arguments.kwargs) }
it { is_expected.to be_true }
end
context "with different arguments" do
let(other) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) }
it { is_expected.to be_false }
end
context "with the same kwargs in a different order" do
let(other) { Spectator::Arguments.new(arguments.positional, {qux: 123, bar: "baz"}) }
it { is_expected.to be_true }
end
context "with a missing kwarg" do
let(other) { Spectator::Arguments.new(arguments.positional, {bar: "baz"}) }
it { is_expected.to be_false }
end
context "with an extra kwarg" do
let(other) { Spectator::Arguments.new(arguments.positional, {bar: "baz", qux: 123, extra: 0}) }
it { is_expected.to be_false }
end
end
context "with FormalArguments" do
context "with equal arguments" do
let(other) { arguments }
it { is_expected.to be_true }
end
context "with different arguments" do
let(other) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, :splat, {1, 2, 3}, {opt: "foobar"}) }
it { is_expected.to be_false }
end
context "with the same kwargs in a different order" do
let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {qux: 123, bar: "baz"}) }
it { is_expected.to be_true }
end
context "with a missing kwarg" do
let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: "baz"}) }
it { is_expected.to be_false }
end
context "with an extra kwarg" do
let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: "baz", qux: 123, extra: 0}) }
it { is_expected.to be_false }
end
context "with different splat arguments" do
let(other) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, {1, 2, 3}, arguments.kwargs) }
it { is_expected.to be_false }
end
context "with mixed positional tuple types" do
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, arguments.splat_name, arguments.splat, arguments.kwargs) }
it { is_expected.to be_true }
end
context "with mixed positional tuple types (flipped)" do
let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {:x, :y, :z}, {bar: "baz", qux: 123}) }
it { is_expected.to be_true }
end
end
end
describe "#===" do
subject { pattern === arguments }
context "with Arguments" do
let(arguments) { Spectator::Arguments.new({42, "foo"}, {bar: "baz", qux: 123}) }
context "with equal arguments" do
let(pattern) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) }
it { is_expected.to be_true }
end
context "with matching arguments" do
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {bar: /baz/, qux: Int32}) }
it { is_expected.to be_true }
end
context "with non-matching arguments" do
let(pattern) { Spectator::FormalArguments.new({arg1: Float64, arg2: /bar/}, {bar: /foo/, qux: "123"}) }
it { is_expected.to be_false }
end
context "with different arguments" do
let(pattern) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, {opt: "foobar"}) }
it { is_expected.to be_false }
end
context "with the same kwargs in a different order" do
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {qux: Int32, bar: /baz/}) }
it { is_expected.to be_true }
end
context "with an additional kwarg" do
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {bar: /baz/}) }
it { is_expected.to be_true }
end
context "with a missing kwarg" do
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, {bar: /baz/, qux: Int32, extra: 0}) }
it { is_expected.to be_false }
end
end
context "with FormalArguments" do
context "with equal arguments" do
let(pattern) { arguments }
it { is_expected.to be_true }
end
context "with matching arguments" do
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, :splat, {Symbol, Symbol, :z}, {bar: /baz/, qux: Int32}) }
it { is_expected.to be_true }
end
context "with non-matching arguments" do
let(pattern) { Spectator::FormalArguments.new({arg1: Float64, arg2: /bar/}, :splat, {String, Int32, :x}, {bar: /foo/, qux: "123"}) }
it { is_expected.to be_false }
end
context "with different arguments" do
let(pattern) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, :splat, {1, 2, 3}, {opt: "foobar"}) }
it { is_expected.to be_false }
end
context "with the same kwargs in a different order" do
let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {qux: Int32, bar: /baz/}) }
it { is_expected.to be_true }
end
context "with an additional kwarg" do
let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: /baz/}) }
it { is_expected.to be_true }
end
context "with a missing kwarg" do
let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: /baz/, qux: Int32, extra: 0}) }
it { is_expected.to be_false }
end
context "with different splat arguments" do
let(pattern) { Spectator::FormalArguments.new(arguments.args, arguments.splat_name, {1, 2, 3}, arguments.kwargs) }
it { is_expected.to be_false }
end
context "with matching mixed positional tuple types" do
let(pattern) { Spectator::FormalArguments.new({arg1: Int32, arg2: /foo/}, arguments.splat_name, arguments.splat, arguments.kwargs) }
it { is_expected.to be_true }
end
context "with non-matching mixed positional tuple types" do
let(pattern) { Spectator::FormalArguments.new({arg1: Float64, arg2: /bar/}, arguments.splat_name, arguments.splat, arguments.kwargs) }
it { is_expected.to be_false }
end
end
end
end

View file

@ -1,5 +1,13 @@
module Spectator module Spectator
# Untyped arguments to a method call (message). # Untyped arguments to a method call (message).
abstract class AbstractArguments abstract class AbstractArguments
# Utility method for comparing two named tuples ignoring order.
private def compare_named_tuples(a : NamedTuple, b : NamedTuple)
a.each do |k, v1|
v2 = b.fetch(k) { return false }
return false unless v1 === v2
end
true
end
end end
end end

View file

@ -4,123 +4,68 @@ module Spectator
# Arguments used in a method call. # Arguments used in a method call.
# #
# Can also be used to match arguments. # Can also be used to match arguments.
# *Args* must be a `Tuple` or `NamedTuple` type representing the standard arguments. # *Args* must be a `Tuple` representing the standard arguments.
# *Splat* must be a `Tuple` type representing the extra positional arguments. # *KWArgs* must be a `NamedTuple` type representing extra keyword arguments.
# *DoubleSplat* must be a `NamedTuple` type representing extra keyword arguments. class Arguments(Args, KWArgs) < AbstractArguments
class Arguments(Args, Splat, DoubleSplat) < AbstractArguments
# Positional arguments. # Positional arguments.
getter args : Args getter args : Args
# Additional positional arguments.
getter splat : Splat
# Keyword arguments. # Keyword arguments.
getter kwargs : DoubleSplat getter kwargs : KWArgs
# Name of the splat argument, if used.
getter splat_name : Symbol?
# Creates arguments used in a method call. # Creates arguments used in a method call.
def initialize(@args : Args, @splat_name : Symbol?, @splat : Splat, @kwargs : DoubleSplat) def initialize(@args : Args, @kwargs : KWArgs)
end {% raise "Positional arguments (generic type Args) must be a Tuple" unless Args <= Tuple %}
{% raise "Keyword arguments (generic type KWArgs) must be a NamedTuple" unless KWArgs <= NamedTuple %}
# Creates arguments used in a method call.
def self.new(args : Args, kwargs : DoubleSplat)
new(args, nil, nil, kwargs)
end end
# Instance of empty arguments. # Instance of empty arguments.
class_getter none : AbstractArguments = build class_getter none : AbstractArguments = capture
# Returns unconstrained arguments. # Returns unconstrained arguments.
def self.any : AbstractArguments? def self.any : AbstractArguments?
nil.as(AbstractArguments?) nil.as(AbstractArguments?)
end end
# Captures arguments passed to a call.
def self.build(args = Tuple.new, kwargs = NamedTuple.new)
new(args, nil, nil, kwargs)
end
# :ditto:
def self.build(args : NamedTuple, splat_name : Symbol, splat : Tuple, kwargs = NamedTuple.new)
new(args, splat_name, splat, kwargs)
end
# Friendlier constructor for capturing arguments. # Friendlier constructor for capturing arguments.
def self.capture(*args, **kwargs) def self.capture(*args, **kwargs)
new(args, nil, nil, kwargs) new(args, kwargs)
end end
# Returns the positional argument at the specified index. # Returns the positional argument at the specified index.
def [](index : Int) def [](index : Int)
positional[index] args[index]
end end
# Returns the specified named argument. # Returns the specified named argument.
def [](arg : Symbol) def [](arg : Symbol)
{% if Args < NamedTuple %}
return @args[arg] if @args.has_key?(arg)
{% end %}
@kwargs[arg] @kwargs[arg]
end end
# Returns all arguments and splatted arguments as a tuple. # Returns all arguments and splatted arguments as a tuple.
def positional : Tuple def positional : Tuple
if (splat = @splat) args
{% if Args < NamedTuple %}args.values{% else %}args{% end %} + splat
else
{% if Args < NamedTuple %}args.values{% else %}args{% end %}
end
end end
# Returns all named positional and keyword arguments as a named tuple. # Returns all named positional and keyword arguments as a named tuple.
def named : NamedTuple def named : NamedTuple
{% if Args < NamedTuple %}
args.merge(kwargs)
{% else %}
kwargs kwargs
{% end %}
end end
# Constructs a string representation of the arguments. # Constructs a string representation of the arguments.
def to_s(io : IO) : Nil def to_s(io : IO) : Nil
return io << "(no args)" if args.empty? && ((splat = @splat).nil? || splat.empty?) && kwargs.empty? return io << "(no args)" if args.empty? && kwargs.empty?
io << '(' io << '('
# Add the positional arguments. # Add the positional arguments.
{% if Args < NamedTuple %}
# Include argument names.
args.each_with_index do |name, value, i|
io << ", " if i > 0
io << name << ": "
value.inspect(io)
end
{% else %}
args.each_with_index do |arg, i| args.each_with_index do |arg, i|
io << ", " if i > 0 io << ", " if i > 0
arg.inspect(io) arg.inspect(io)
end end
{% end %}
# Add the splat arguments.
if (splat = @splat) && !splat.empty?
io << ", " unless args.empty?
if splat_name = !args.empty? && @splat_name
io << '*' << splat_name << ": {"
end
splat.each_with_index do |arg, i|
io << ", " if i > 0
arg.inspect(io)
end
io << '}' if splat_name
end
# Add the keyword arguments. # Add the keyword arguments.
offset = args.size kwargs.each_with_index(args.size) do |key, value, i|
offset += splat.size if (splat = @splat)
kwargs.each_with_index(offset) do |key, value, i|
io << ", " if i > 0 io << ", " if i > 0
io << key << ": " io << key << ": "
value.inspect(io) value.inspect(io)
@ -130,55 +75,35 @@ module Spectator
end end
# Checks if this set of arguments and another are equal. # Checks if this set of arguments and another are equal.
def ==(other : Arguments) def ==(other : AbstractArguments)
positional == other.positional && kwargs == other.kwargs positional == other.positional && kwargs == other.kwargs
end end
# Checks if another set of arguments matches this set of arguments. # Checks if another set of arguments matches this set of arguments.
def ===(other : Arguments) def ===(other : Arguments)
self_args = args positional === other.positional && compare_named_tuples(kwargs, other.kwargs)
other_args = other.args
case {self_args, other_args}
when {Tuple, Tuple} then compare(positional, other.positional, kwargs, other.kwargs)
when {Tuple, NamedTuple} then compare(kwargs, other.named, positional, other_args, other.splat)
when {NamedTuple, Tuple} then compare(positional, other.positional, kwargs, other.kwargs)
else
self_args === other_args && (!splat || splat === other.splat) && compare_named_tuples(kwargs, other.kwargs)
end
end end
private def compare(self_positional : Tuple, other_positional : Tuple, self_kwargs : NamedTuple, other_kwargs : NamedTuple) # :ditto:
self_positional === other_positional && compare_named_tuples(self_kwargs, other_kwargs) def ===(other : FormalArguments)
end return false unless compare_named_tuples(kwargs, other.named)
private def compare(self_kwargs : NamedTuple, other_named : NamedTuple, self_positional : Tuple, other_args : NamedTuple, other_splat : Tuple?)
return false unless compare_named_tuples(self_kwargs, other_named)
i = 0 i = 0
other_args.each do |k, v2| other.args.each do |k, v2|
next if self_kwargs.has_key?(k) # Covered by named arguments. next if kwargs.has_key?(k) # Covered by named arguments.
v1 = self_positional.fetch(i) { return false } v1 = positional.fetch(i) { return false }
i += 1 i += 1
return false unless v1 === v2 return false unless v1 === v2
end end
other_splat.try &.each do |v2| other.splat.try &.each do |v2|
v1 = self_positional.fetch(i) { return false } v1 = positional.fetch(i) { return false }
i += 1 i += 1
return false unless v1 === v2 return false unless v1 === v2
end end
i == self_positional.size i == positional.size
end
private def compare_named_tuples(a : NamedTuple, b : NamedTuple)
a.each do |k, v1|
v2 = b.fetch(k) { return false }
return false unless v1 === v2
end
true
end end
end end
end end

View file

@ -0,0 +1,133 @@
require "./abstract_arguments"
module Spectator
# Arguments passed into a method.
#
# *Args* must be a `NamedTuple` type representing the standard arguments.
# *Splat* must be a `Tuple` type representing the extra positional arguments.
# *DoubleSplat* must be a `NamedTuple` type representing extra keyword arguments.
class FormalArguments(Args, Splat, DoubleSplat) < AbstractArguments
# Positional arguments.
getter args : Args
# Additional positional arguments.
getter splat : Splat
# Keyword arguments.
getter kwargs : DoubleSplat
# Name of the splat argument, if used.
getter splat_name : Symbol?
# Creates arguments used in a method call.
def initialize(@args : Args, @splat_name : Symbol?, @splat : Splat, @kwargs : DoubleSplat)
{% raise "Positional arguments (generic type Args) must be a NamedTuple" unless Args <= NamedTuple %}
{% raise "Splat arguments (generic type Splat) must be a Tuple" unless Splat <= Tuple || Splat <= Nil %}
{% raise "Keyword arguments (generic type DoubleSplat) must be a NamedTuple" unless DoubleSplat <= NamedTuple %}
end
# Creates arguments used in a method call.
def self.new(args : Args, kwargs : DoubleSplat)
new(args, nil, nil, kwargs)
end
# Captures arguments passed to a call.
def self.build(args = NamedTuple.new, kwargs = NamedTuple.new)
new(args, nil, nil, kwargs)
end
# :ditto:
def self.build(args : NamedTuple, splat_name : Symbol, splat : Tuple, kwargs = NamedTuple.new)
new(args, splat_name, splat, kwargs)
end
# Instance of empty arguments.
class_getter none : AbstractArguments = build
# Returns the positional argument at the specified index.
def [](index : Int)
positional[index]
end
# Returns the specified named argument.
def [](arg : Symbol)
return @args[arg] if @args.has_key?(arg)
@kwargs[arg]
end
# Returns all arguments and splatted arguments as a tuple.
def positional : Tuple
if (splat = @splat)
args.values + splat
else
args.values
end
end
# Returns all named positional and keyword arguments as a named tuple.
def named : NamedTuple
args.merge(kwargs)
end
# Constructs a string representation of the arguments.
def to_s(io : IO) : Nil
return io << "(no args)" if args.empty? && ((splat = @splat).nil? || splat.empty?) && kwargs.empty?
io << '('
# Add the positional arguments.
{% if Args < NamedTuple %}
# Include argument names.
args.each_with_index do |name, value, i|
io << ", " if i > 0
io << name << ": "
value.inspect(io)
end
{% else %}
args.each_with_index do |arg, i|
io << ", " if i > 0
arg.inspect(io)
end
{% end %}
# Add the splat arguments.
if (splat = @splat) && !splat.empty?
io << ", " unless args.empty?
if splat_name = !args.empty? && @splat_name
io << '*' << splat_name << ": {"
end
splat.each_with_index do |arg, i|
io << ", " if i > 0
arg.inspect(io)
end
io << '}' if splat_name
end
# Add the keyword arguments.
offset = args.size
offset += splat.size if (splat = @splat)
kwargs.each_with_index(offset) do |key, value, i|
io << ", " if i > 0
io << key << ": "
value.inspect(io)
end
io << ')'
end
# Checks if this set of arguments and another are equal.
def ==(other : AbstractArguments)
positional == other.positional && kwargs == other.kwargs
end
# Checks if another set of arguments matches this set of arguments.
def ===(other : Arguments)
positional === other.positional && compare_named_tuples(kwargs, other.kwargs)
end
# :ditto:
def ===(other : FormalArguments)
compare_named_tuples(args, other.args) && splat === other.splat && compare_named_tuples(kwargs, other.kwargs)
end
end
end

View file

@ -1,5 +1,6 @@
require "./abstract_arguments" require "./abstract_arguments"
require "./arguments" require "./arguments"
require "./formal_arguments"
module Spectator module Spectator
# Stores information about a call to a method. # Stores information about a call to a method.
@ -16,7 +17,14 @@ module Spectator
# Creates a method call by splatting its arguments. # Creates a method call by splatting its arguments.
def self.capture(method : Symbol, *args, **kwargs) def self.capture(method : Symbol, *args, **kwargs)
arguments = Arguments.new(args, kwargs).as(AbstractArguments) arguments = Arguments.capture(*args, **kwargs).as(AbstractArguments)
new(method, arguments)
end
# Creates a method call from within a method.
# Takes the same arguments as `FormalArguments.build` but with the method name first.
def self.build(method : Symbol, *args, **kwargs)
arguments = FormalArguments.build(*args, **kwargs).as(AbstractArguments)
new(method, arguments) new(method, arguments)
end end

View file

@ -1,5 +1,5 @@
require "../dsl/reserved" require "../dsl/reserved"
require "./arguments" require "./formal_arguments"
require "./method_call" require "./method_call"
require "./stub" require "./stub"
require "./typed_stub" require "./typed_stub"
@ -140,7 +140,8 @@ module Spectator
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %} ){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
# Capture information about the call. # Capture information about the call.
%args = ::Spectator::Arguments.build( %call = ::Spectator::MethodCall.build(
{{method.name.symbolize}},
::NamedTuple.new( ::NamedTuple.new(
{% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %} {% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
), ),
@ -149,7 +150,6 @@ module Spectator
{% for arg, i in method.args %}{% if method.splat_index && i > method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %} {% for arg, i in method.args %}{% if method.splat_index && i > method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
).merge({{method.double_splat}}) ).merge({{method.double_splat}})
) )
%call = ::Spectator::MethodCall.new({{method.name.symbolize}}, %args)
_spectator_record_call(%call) _spectator_record_call(%call)
# Attempt to find a stub that satisfies the method call and arguments. # Attempt to find a stub that satisfies the method call and arguments.
@ -242,7 +242,8 @@ module Spectator
){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %} ){% if method.return_type %} : {{method.return_type}}{% end %}{% if !method.free_vars.empty? %} forall {{method.free_vars.splat}}{% end %}
# Capture information about the call. # Capture information about the call.
%args = ::Spectator::Arguments.build( %call = ::Spectator::MethodCall.build(
{{method.name.symbolize}},
::NamedTuple.new( ::NamedTuple.new(
{% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %} {% for arg, i in method.args %}{% if !method.splat_index || i < method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
), ),
@ -251,7 +252,6 @@ module Spectator
{% for arg, i in method.args %}{% if method.splat_index && i > method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %} {% for arg, i in method.args %}{% if method.splat_index && i > method.splat_index %}{{arg.internal_name.stringify}}: {{arg.internal_name}}, {% end %}{% end %}
).merge({{method.double_splat}}) ).merge({{method.double_splat}})
) )
%call = ::Spectator::MethodCall.new({{method.name.symbolize}}, %args)
_spectator_record_call(%call) _spectator_record_call(%call)
# Attempt to find a stub that satisfies the method call and arguments. # Attempt to find a stub that satisfies the method call and arguments.