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"
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
expect(arguments).to have_attributes(
args: {42, "foo"},
splat_name: :splat,
splat: {:x, :y, :z},
kwargs: {bar: "baz", qux: 123}
)
end
@ -27,33 +25,6 @@ Spectator.describe Spectator::Arguments do
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::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
describe "#[](symbol)" do
@ -63,31 +34,13 @@ Spectator.describe Spectator::Arguments do
expect(arguments[:qux]).to eq(123)
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
describe "#to_s" do
subject { arguments.to_s }
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
context "when empty" do
@ -97,172 +50,235 @@ Spectator.describe Spectator::Arguments do
is_expected.to eq("(no args)")
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
describe "#==" do
subject { arguments == other }
context "with equal arguments" do
let(other) { arguments }
context "with Arguments" do
context "with equal arguments" do
let(other) { arguments }
it { is_expected.to be_true }
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.args, {qux: 123, bar: "baz"}) }
it { is_expected.to be_true }
end
context "with a missing kwarg" do
let(other) { Spectator::Arguments.new(arguments.args, {bar: "baz"}) }
it { is_expected.to be_false }
end
context "with an extra kwarg" do
let(other) { Spectator::Arguments.new(arguments.args, {bar: "baz", qux: 123, extra: 0}) }
it { is_expected.to be_false }
end
end
context "with different arguments" do
let(other) { Spectator::Arguments.new({123, :foo, "bar"}, :splat, {1, 2, 3}, {opt: "foobar"}) }
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_false }
end
it { is_expected.to be_true }
end
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"}) }
context "with different arguments" do
let(other) { Spectator::FormalArguments.new({arg1: 123, arg2: :foo, arg3: "bar"}, {opt: "foobar"}) }
it { is_expected.to be_true }
end
it { is_expected.to be_false }
end
context "with a missing kwarg" do
let(other) { Spectator::Arguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: "baz"}) }
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_false }
end
it { is_expected.to be_true }
end
context "with an extra kwarg" do
let(other) { Spectator::Arguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: "baz", qux: 123, extra: 0}) }
context "with a missing kwarg" do
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz"}) }
it { is_expected.to be_false }
end
it { is_expected.to be_false }
end
context "with different splat arguments" do
let(other) { Spectator::Arguments.new(arguments.args, arguments.splat_name, {1, 2, 3}, arguments.kwargs) }
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 }
end
it { is_expected.to be_false }
end
context "with mixed positional tuple types" do
let(other) { Spectator::Arguments.new({arg1: 42, arg2: "foo"}, arguments.splat_name, arguments.splat, arguments.kwargs) }
context "with different splat arguments" do
let(other) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, :splat, {1, 2, 3}, {bar: "baz", qux: 123}) }
it { is_expected.to be_true }
end
it { is_expected.to be_false }
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}) }
context "with mixed positional tuple types" do
let(other) { Spectator::FormalArguments.new({arg1: 42}, :splat, {"foo"}, {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 }
it { is_expected.to be_true }
end
end
end
describe "#===" do
subject { pattern === arguments }
context "with equal arguments" do
let(pattern) { arguments }
context "with Arguments" do
context "with equal arguments" do
let(pattern) { arguments }
it { is_expected.to be_true }
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.args, {qux: Int32, bar: /baz/}) }
it { is_expected.to be_true }
end
context "with an additional kwarg" do
let(pattern) { Spectator::Arguments.new(arguments.args, {bar: /baz/}) }
it { is_expected.to be_true }
end
context "with a missing kwarg" do
let(pattern) { Spectator::Arguments.new(arguments.args, {bar: /baz/, qux: Int32, extra: 0}) }
it { is_expected.to be_false }
end
end
context "with matching arguments" do
let(pattern) { Spectator::Arguments.new({Int32, /foo/}, :splat, {Symbol, Symbol, :z}, {bar: /baz/, qux: Int32}) }
context "with FormalArguments" do
let(arguments) { Spectator::FormalArguments.new({arg1: 42, arg2: "foo"}, {bar: "baz", qux: 123}) }
it { is_expected.to be_true }
end
context "with equal arguments" do
let(pattern) { Spectator::Arguments.new({42, "foo"}, {bar: "baz", qux: 123}) }
context "with non-matching arguments" do
let(pattern) { Spectator::Arguments.new({Float64, /bar/}, :splat, {String, Int32, :x}, {bar: /foo/, qux: "123"}) }
it { is_expected.to be_true }
end
it { is_expected.to be_false }
end
context "with matching arguments" do
let(pattern) { Spectator::Arguments.new({Int32, /foo/}, {bar: /baz/, qux: Int32}) }
context "with different arguments" do
let(pattern) { Spectator::Arguments.new({123, :foo, "bar"}, :splat, {1, 2, 3}, {opt: "foobar"}) }
it { is_expected.to be_true }
end
it { is_expected.to be_false }
end
context "with non-matching arguments" do
let(pattern) { Spectator::Arguments.new({Float64, /bar/}, {bar: /foo/, qux: "123"}) }
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/}) }
it { is_expected.to be_false }
end
it { is_expected.to be_true }
end
context "with different arguments" do
let(pattern) { Spectator::Arguments.new({123, :foo, "bar"}, {opt: "foobar"}) }
context "with an additional kwarg" do
let(pattern) { Spectator::Arguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: /baz/}) }
it { is_expected.to be_false }
end
it { is_expected.to be_true }
end
context "with the same kwargs in a different order" do
let(pattern) { Spectator::Arguments.new(arguments.positional, {qux: Int32, bar: /baz/}) }
context "with a missing kwarg" do
let(pattern) { Spectator::Arguments.new(arguments.args, arguments.splat_name, arguments.splat, {bar: /baz/, qux: Int32, extra: 0}) }
it { is_expected.to be_true }
end
it { is_expected.to be_false }
end
context "with an additional kwarg" do
let(pattern) { Spectator::Arguments.new(arguments.positional, {bar: /baz/}) }
context "with different splat arguments" do
let(pattern) { Spectator::Arguments.new(arguments.args, arguments.splat_name, {1, 2, 3}, arguments.kwargs) }
it { is_expected.to be_true }
end
it { is_expected.to be_false }
end
context "with a missing kwarg" do
let(pattern) { Spectator::Arguments.new(arguments.positional, {bar: /baz/, qux: Int32, extra: 0}) }
context "with matching mixed positional tuple types" do
let(pattern) { Spectator::Arguments.new({arg1: Int32, arg2: /foo/}, arguments.splat_name, arguments.splat, arguments.kwargs) }
it { is_expected.to be_false }
end
it { is_expected.to be_true }
end
context "with different splat arguments" do
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) }
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) }
it { is_expected.to be_false }
end
it { is_expected.to be_false }
end
context "with matching mixed positional tuple types" do
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) }
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(pattern) { Spectator::Arguments.capture(Int32, /foo/, Symbol, Symbol, :z, bar: /baz/, qux: Int32) }
it { is_expected.to be_true }
end
it { is_expected.to be_true }
end
context "with non-matching mixed positional tuple types" do
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) }
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(pattern) { Spectator::Arguments.capture(Float64, /bar/, Symbol, String, :z, bar: /foo/, qux: Int32) }
it { is_expected.to be_false }
end
it { is_expected.to be_false }
end
context "with matching args spilling over into splat and mixed positional tuple types" do
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) }
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(pattern) { Spectator::Arguments.capture(/foo/, Symbol, :y, Symbol, arg1: Int32, bar: /baz/, qux: 123) }
it { is_expected.to be_true }
end
it { is_expected.to be_true }
end
context "with non-matching args spilling over into splat and mixed positional tuple types" do
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) }
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(pattern) { Spectator::Arguments.capture(5, Symbol, :z, Symbol, arg2: /foo/, bar: /baz/, qux: Int32) }
it { is_expected.to be_false }
end
it { is_expected.to be_false }
end
context "with matching mixed named positional and keyword arguments" do
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) }
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(pattern) { Spectator::Arguments.capture(/bar/, String, :y, Symbol, arg1: 0, bar: /foo/, qux: Float64) }
it { is_expected.to be_true }
end
it { is_expected.to be_false }
context "with non-matching mixed named positional and keyword arguments" do
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) }
it { is_expected.to be_false }
end
context "with non-matching mixed named positional and keyword arguments" do
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) }
it { is_expected.to be_false }
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
# Untyped arguments to a method call (message).
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

View file

@ -4,123 +4,68 @@ module Spectator
# Arguments used in a method call.
#
# Can also be used to match arguments.
# *Args* must be a `Tuple` or `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 Arguments(Args, Splat, DoubleSplat) < AbstractArguments
# *Args* must be a `Tuple` representing the standard arguments.
# *KWArgs* must be a `NamedTuple` type representing extra keyword arguments.
class Arguments(Args, KWArgs) < 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?
getter kwargs : KWArgs
# Creates arguments used in a method call.
def initialize(@args : Args, @splat_name : Symbol?, @splat : Splat, @kwargs : DoubleSplat)
end
# Creates arguments used in a method call.
def self.new(args : Args, kwargs : DoubleSplat)
new(args, nil, nil, kwargs)
def initialize(@args : Args, @kwargs : KWArgs)
{% 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 %}
end
# Instance of empty arguments.
class_getter none : AbstractArguments = build
class_getter none : AbstractArguments = capture
# Returns unconstrained arguments.
def self.any : AbstractArguments?
nil.as(AbstractArguments?)
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.
def self.capture(*args, **kwargs)
new(args, nil, nil, kwargs)
new(args, kwargs)
end
# Returns the positional argument at the specified index.
def [](index : Int)
positional[index]
args[index]
end
# Returns the specified named argument.
def [](arg : Symbol)
{% if Args < NamedTuple %}
return @args[arg] if @args.has_key?(arg)
{% end %}
@kwargs[arg]
end
# Returns all arguments and splatted arguments as a tuple.
def positional : Tuple
if (splat = @splat)
{% if Args < NamedTuple %}args.values{% else %}args{% end %} + splat
else
{% if Args < NamedTuple %}args.values{% else %}args{% end %}
end
args
end
# Returns all named positional and keyword arguments as a named tuple.
def named : NamedTuple
{% if Args < NamedTuple %}
args.merge(kwargs)
{% else %}
kwargs
{% end %}
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?
return io << "(no args)" if args.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
args.each_with_index do |arg, i|
io << ", " if i > 0
arg.inspect(io)
end
# Add the keyword arguments.
offset = args.size
offset += splat.size if (splat = @splat)
kwargs.each_with_index(offset) do |key, value, i|
kwargs.each_with_index(args.size) do |key, value, i|
io << ", " if i > 0
io << key << ": "
value.inspect(io)
@ -130,55 +75,35 @@ module Spectator
end
# Checks if this set of arguments and another are equal.
def ==(other : Arguments)
def ==(other : AbstractArguments)
positional == other.positional && kwargs == other.kwargs
end
# Checks if another set of arguments matches this set of arguments.
def ===(other : Arguments)
self_args = args
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
positional === other.positional && compare_named_tuples(kwargs, other.kwargs)
end
private def compare(self_positional : Tuple, other_positional : Tuple, self_kwargs : NamedTuple, other_kwargs : NamedTuple)
self_positional === other_positional && compare_named_tuples(self_kwargs, other_kwargs)
end
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)
# :ditto:
def ===(other : FormalArguments)
return false unless compare_named_tuples(kwargs, other.named)
i = 0
other_args.each do |k, v2|
next if self_kwargs.has_key?(k) # Covered by named arguments.
other.args.each do |k, v2|
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
return false unless v1 === v2
end
other_splat.try &.each do |v2|
v1 = self_positional.fetch(i) { return false }
other.splat.try &.each do |v2|
v1 = positional.fetch(i) { return false }
i += 1
return false unless v1 === v2
end
i == self_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
i == positional.size
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 "./arguments"
require "./formal_arguments"
module Spectator
# Stores information about a call to a method.
@ -16,7 +17,14 @@ module Spectator
# Creates a method call by splatting its arguments.
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)
end

View file

@ -1,5 +1,5 @@
require "../dsl/reserved"
require "./arguments"
require "./formal_arguments"
require "./method_call"
require "./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 %}
# Capture information about the call.
%args = ::Spectator::Arguments.build(
%call = ::Spectator::MethodCall.build(
{{method.name.symbolize}},
::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 %}
),
@ -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 %}
).merge({{method.double_splat}})
)
%call = ::Spectator::MethodCall.new({{method.name.symbolize}}, %args)
_spectator_record_call(%call)
# 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 %}
# Capture information about the call.
%args = ::Spectator::Arguments.build(
%call = ::Spectator::MethodCall.build(
{{method.name.symbolize}},
::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 %}
),
@ -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 %}
).merge({{method.double_splat}})
)
%call = ::Spectator::MethodCall.new({{method.name.symbolize}}, %args)
_spectator_record_call(%call)
# Attempt to find a stub that satisfies the method call and arguments.