Skip to content

Commit f3f9950

Browse files
committed
Add a reflection API for determining the fields of a node
1 parent f9a1abb commit f3f9950

File tree

19 files changed

+427
-53
lines changed

19 files changed

+427
-53
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ out.svg
4747
/lib/prism/dsl.rb
4848
/lib/prism/mutation_compiler.rb
4949
/lib/prism/node.rb
50+
/lib/prism/reflection.rb
5051
/lib/prism/serialize.rb
5152
/lib/prism/visitor.rb
5253
/sorbet/

docs/configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ A lot of code in prism's repository is templated from a single configuration fil
1616
* `lib/prism/dsl.rb` - for defining the DSL for the nodes in Ruby
1717
* `lib/prism/mutation_compiler.rb` - for defining the mutation compiler for the nodes in Ruby
1818
* `lib/prism/node.rb` - for defining the nodes in Ruby
19+
* `lib/prism/reflection.rb` - for defining the reflection API in Ruby
1920
* `lib/prism/serialize.rb` - for defining how to deserialize the nodes in Ruby
2021
* `lib/prism/visitor.rb` - for defining the visitor interface for the nodes in Ruby
2122
* `src/diagnostic.c` - for defining how to build diagnostics

ext/prism/extension.c

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -969,7 +969,7 @@ parse_input_success_p(pm_string_t *input, const pm_options_t *options) {
969969

970970
/**
971971
* call-seq:
972-
* Prism::parse_success?(source, **options) -> Array
972+
* Prism::parse_success?(source, **options) -> bool
973973
*
974974
* Parse the given string and return true if it parses without errors. For
975975
* supported options, see Prism::parse.
@@ -989,7 +989,19 @@ parse_success_p(int argc, VALUE *argv, VALUE self) {
989989

990990
/**
991991
* call-seq:
992-
* Prism::parse_file_success?(filepath, **options) -> Array
992+
* Prism::parse_failure?(source, **options) -> bool
993+
*
994+
* Parse the given string and return true if it parses with errors. For
995+
* supported options, see Prism::parse.
996+
*/
997+
static VALUE
998+
parse_failure_p(int argc, VALUE *argv, VALUE self) {
999+
return RTEST(parse_success_p(argc, argv, self)) ? Qfalse : Qtrue;
1000+
}
1001+
1002+
/**
1003+
* call-seq:
1004+
* Prism::parse_file_success?(filepath, **options) -> bool
9931005
*
9941006
* Parse the given file and return true if it parses without errors. For
9951007
* supported options, see Prism::parse.
@@ -1008,6 +1020,18 @@ parse_file_success_p(int argc, VALUE *argv, VALUE self) {
10081020
return result;
10091021
}
10101022

1023+
/**
1024+
* call-seq:
1025+
* Prism::parse_file_failure?(filepath, **options) -> bool
1026+
*
1027+
* Parse the given file and return true if it parses with errors. For
1028+
* supported options, see Prism::parse.
1029+
*/
1030+
static VALUE
1031+
parse_file_failure_p(int argc, VALUE *argv, VALUE self) {
1032+
return RTEST(parse_file_success_p(argc, argv, self)) ? Qfalse : Qtrue;
1033+
}
1034+
10111035
/******************************************************************************/
10121036
/* Utility functions exposed to make testing easier */
10131037
/******************************************************************************/
@@ -1366,7 +1390,9 @@ Init_prism(void) {
13661390
rb_define_singleton_method(rb_cPrism, "parse_lex", parse_lex, -1);
13671391
rb_define_singleton_method(rb_cPrism, "parse_lex_file", parse_lex_file, -1);
13681392
rb_define_singleton_method(rb_cPrism, "parse_success?", parse_success_p, -1);
1393+
rb_define_singleton_method(rb_cPrism, "parse_failure?", parse_failure_p, -1);
13691394
rb_define_singleton_method(rb_cPrism, "parse_file_success?", parse_file_success_p, -1);
1395+
rb_define_singleton_method(rb_cPrism, "parse_file_failure?", parse_file_failure_p, -1);
13701396

13711397
#ifndef PRISM_EXCLUDE_SERIALIZATION
13721398
rb_define_singleton_method(rb_cPrism, "dump", dump, -1);

lib/prism.rb

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ module Prism
2424
autoload :NodeInspector, "prism/node_inspector"
2525
autoload :Pack, "prism/pack"
2626
autoload :Pattern, "prism/pattern"
27+
autoload :Reflection, "prism/reflection"
2728
autoload :Serialize, "prism/serialize"
2829
autoload :Translation, "prism/translation"
2930
autoload :Visitor, "prism/visitor"
@@ -64,22 +65,6 @@ def self.lex_ripper(source)
6465
def self.load(source, serialized)
6566
Serialize.load(source, serialized)
6667
end
67-
68-
# :call-seq:
69-
# Prism::parse_failure?(source, **options) -> bool
70-
#
71-
# Returns true if the source parses with errors.
72-
def self.parse_failure?(source, **options)
73-
!parse_success?(source, **options)
74-
end
75-
76-
# :call-seq:
77-
# Prism::parse_file_failure?(filepath, **options) -> bool
78-
#
79-
# Returns true if the file at filepath parses with errors.
80-
def self.parse_file_failure?(filepath, **options)
81-
!parse_file_success?(filepath, **options)
82-
end
8368
end
8469

8570
require_relative "prism/node"

lib/prism/ffi.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,12 +286,22 @@ def parse_success?(code, **options)
286286
LibRubyParser::PrismString.with_string(code) { |string| parse_file_success_common(string, options) }
287287
end
288288

289+
# Mirror the Prism.parse_failure? API by using the serialization API.
290+
def parse_failure?(code, **options)
291+
!parse_success?(code, **options)
292+
end
293+
289294
# Mirror the Prism.parse_file_success? API by using the serialization API.
290295
def parse_file_success?(filepath, **options)
291296
options[:filepath] = filepath
292297
LibRubyParser::PrismString.with_file(filepath) { |string| parse_file_success_common(string, options) }
293298
end
294299

300+
# Mirror the Prism.parse_file_failure? API by using the serialization API.
301+
def parse_file_failure?(filepath, **options)
302+
!parse_file_success?(filepath, **options)
303+
end
304+
295305
private
296306

297307
def dump_common(string, options) # :nodoc:

prism.gemspec

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ Gem::Specification.new do |spec|
8888
"lib/prism/parse_result/newlines.rb",
8989
"lib/prism/pattern.rb",
9090
"lib/prism/polyfill/string.rb",
91+
"lib/prism/reflection.rb",
9192
"lib/prism/serialize.rb",
9293
"lib/prism/translation.rb",
9394
"lib/prism/translation/parser.rb",
@@ -136,6 +137,7 @@ Gem::Specification.new do |spec|
136137
"sig/prism/pack.rbs",
137138
"sig/prism/parse_result.rbs",
138139
"sig/prism/pattern.rbs",
140+
"sig/prism/reflection.rbs",
139141
"sig/prism/serialize.rbs",
140142
"sig/prism/visitor.rbs",
141143
"rbi/prism.rbi",
@@ -145,6 +147,7 @@ Gem::Specification.new do |spec|
145147
"rbi/prism/node_ext.rbi",
146148
"rbi/prism/node.rbi",
147149
"rbi/prism/parse_result.rbi",
150+
"rbi/prism/reflection.rbi",
148151
"rbi/prism/translation/parser/compiler.rbi",
149152
"rbi/prism/translation/ripper.rbi",
150153
"rbi/prism/translation/ripper/ripper_compiler.rbi",

rbi/prism.rbi

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,57 @@
11
# typed: strict
22

33
module Prism
4-
sig { params(source: String, filepath: T.nilable(String), line: T.nilable(Integer), offset: T.nilable(Integer), encoding: T.nilable(Encoding), frozen_string_literal: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean), scopes: T.nilable(T::Array[T::Array[Symbol]])).returns(String) }
5-
def self.dump(source, filepath: nil, line: nil, offset: nil, encoding: nil, frozen_string_literal: nil, verbose: nil, scopes: nil); end
4+
sig { params(source: String, command_line: T.nilable(String), encoding: T.nilable(T.any(String, Encoding)), filepath: T.nilable(String), frozen_string_literal: T.nilable(T::Boolean), line: T.nilable(Integer), scopes: T.nilable(T::Array[T::Array[Symbol]]), version: T.nilable(String)).returns(String) }
5+
def self.dump(source, command_line: nil, encoding: nil, filepath: nil, frozen_string_literal: nil, line: nil, scopes: nil, version: nil); end
66

7-
sig { params(filepath: String, line: T.nilable(Integer), offset: T.nilable(Integer), encoding: T.nilable(Encoding), frozen_string_literal: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean), scopes: T.nilable(T::Array[T::Array[Symbol]])).returns(String) }
8-
def self.dump_file(filepath, line: nil, offset: nil, encoding: nil, frozen_string_literal: nil, verbose: nil, scopes: nil); end
7+
sig { params(filepath: String, command_line: T.nilable(String), encoding: T.nilable(T.any(String, Encoding)), frozen_string_literal: T.nilable(T::Boolean), line: T.nilable(Integer), scopes: T.nilable(T::Array[T::Array[Symbol]]), version: T.nilable(String)).returns(String) }
8+
def self.dump_file(filepath, command_line: nil, encoding: nil, frozen_string_literal: nil, line: nil, scopes: nil, version: nil); end
99

10-
sig { params(source: String, filepath: T.nilable(String), line: T.nilable(Integer), offset: T.nilable(Integer), encoding: T.nilable(Encoding), frozen_string_literal: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean), scopes: T.nilable(T::Array[T::Array[Symbol]])).returns(Prism::ParseResult[T::Array[T.untyped]]) }
11-
def self.lex(source, filepath: nil, line: nil, offset: nil, encoding: nil, frozen_string_literal: nil, verbose: nil, scopes: nil); end
10+
sig { params(source: String, command_line: T.nilable(String), encoding: T.nilable(T.any(String, Encoding)), filepath: T.nilable(String), frozen_string_literal: T.nilable(T::Boolean), line: T.nilable(Integer), scopes: T.nilable(T::Array[T::Array[Symbol]]), version: T.nilable(String)).returns(Prism::ParseResult[T::Array[T.untyped]]) }
11+
def self.lex(source, command_line: nil, encoding: nil, filepath: nil, frozen_string_literal: nil, line: nil, scopes: nil, version: nil); end
1212

13-
sig { params(filepath: String, line: T.nilable(Integer), offset: T.nilable(Integer), encoding: T.nilable(Encoding), frozen_string_literal: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean), scopes: T.nilable(T::Array[T::Array[Symbol]])).returns(Prism::ParseResult[T::Array[T.untyped]]) }
14-
def self.lex_file(filepath, line: nil, offset: nil, encoding: nil, frozen_string_literal: nil, verbose: nil, scopes: nil); end
13+
sig { params(filepath: String, command_line: T.nilable(String), encoding: T.nilable(T.any(String, Encoding)), frozen_string_literal: T.nilable(T::Boolean), line: T.nilable(Integer), scopes: T.nilable(T::Array[T::Array[Symbol]]), version: T.nilable(String)).returns(Prism::ParseResult[T::Array[T.untyped]]) }
14+
def self.lex_file(filepath, command_line: nil, encoding: nil, frozen_string_literal: nil, line: nil, scopes: nil, version: nil); end
1515

16-
sig { params(source: String, filepath: T.nilable(String), line: T.nilable(Integer), offset: T.nilable(Integer), encoding: T.nilable(Encoding), frozen_string_literal: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean), scopes: T.nilable(T::Array[T::Array[Symbol]])).returns(Prism::ParseResult[T::Array[T.untyped]]) }
17-
def self.lex_compat(source, filepath: nil, line: nil, offset: nil, encoding: nil, frozen_string_literal: nil, verbose: nil, scopes: nil); end
16+
sig { params(source: String, options: T::Hash[Symbol, T.untyped]).returns(Prism::ParseResult[T::Array[T.untyped]]) }
17+
def self.lex_compat(source, **options); end
1818

1919
sig { params(source: String).returns(T::Array[T.untyped]) }
2020
def self.lex_ripper(source); end
2121

2222
sig { params(source: String, serialized: String).returns(Prism::ParseResult[Prism::ProgramNode]) }
2323
def self.load(source, serialized); end
2424

25-
sig { params(source: String, filepath: T.nilable(String), line: T.nilable(Integer), offset: T.nilable(Integer), encoding: T.nilable(Encoding), frozen_string_literal: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean), scopes: T.nilable(T::Array[T::Array[Symbol]])).returns(Prism::ParseResult[Prism::ProgramNode]) }
26-
def self.parse(source, filepath: nil, line: nil, offset: nil, encoding: nil, frozen_string_literal: nil, verbose: nil, scopes: nil); end
25+
sig { params(source: String, command_line: T.nilable(String), encoding: T.nilable(T.any(String, Encoding)), filepath: T.nilable(String), frozen_string_literal: T.nilable(T::Boolean), line: T.nilable(Integer), scopes: T.nilable(T::Array[T::Array[Symbol]]), version: T.nilable(String)).returns(Prism::ParseResult[Prism::ProgramNode]) }
26+
def self.parse(source, command_line: nil, encoding: nil, filepath: nil, frozen_string_literal: nil, line: nil, scopes: nil, version: nil); end
2727

28-
sig { params(filepath: String, line: T.nilable(Integer), offset: T.nilable(Integer), encoding: T.nilable(Encoding), frozen_string_literal: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean), scopes: T.nilable(T::Array[T::Array[Symbol]])).returns(Prism::ParseResult[Prism::ProgramNode]) }
29-
def self.parse_file(filepath, line: nil, offset: nil, encoding: nil, frozen_string_literal: nil, verbose: nil, scopes: nil); end
28+
sig { params(filepath: String, command_line: T.nilable(String), encoding: T.nilable(T.any(String, Encoding)), frozen_string_literal: T.nilable(T::Boolean), line: T.nilable(Integer), scopes: T.nilable(T::Array[T::Array[Symbol]]), version: T.nilable(String)).returns(Prism::ParseResult[Prism::ProgramNode]) }
29+
def self.parse_file(filepath, command_line: nil, encoding: nil, frozen_string_literal: nil, line: nil, scopes: nil, version: nil); end
3030

31-
sig { params(stream: T.any(IO, StringIO), filepath: T.nilable(String), line: T.nilable(Integer), offset: T.nilable(Integer), encoding: T.nilable(Encoding), frozen_string_literal: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean), scopes: T.nilable(T::Array[T::Array[Symbol]])).returns(Prism::ParseResult[Prism::ProgramNode]) }
32-
def self.parse_stream(stream, filepath: nil, line: nil, offset: nil, encoding: nil, frozen_string_literal: nil, verbose: nil, scopes: nil); end
31+
sig { params(stream: T.any(IO, StringIO), command_line: T.nilable(String), encoding: T.nilable(T.any(String, Encoding)), filepath: T.nilable(String), frozen_string_literal: T.nilable(T::Boolean), line: T.nilable(Integer), scopes: T.nilable(T::Array[T::Array[Symbol]]), version: T.nilable(String)).returns(Prism::ParseResult[Prism::ProgramNode]) }
32+
def self.parse_stream(stream, command_line: nil, encoding: nil, filepath: nil, frozen_string_literal: nil, line: nil, scopes: nil, version: nil); end
3333

34-
sig { params(source: String, filepath: T.nilable(String), line: T.nilable(Integer), offset: T.nilable(Integer), encoding: T.nilable(Encoding), frozen_string_literal: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean), scopes: T.nilable(T::Array[T::Array[Symbol]])).returns(T::Array[Prism::Comment]) }
35-
def self.parse_comments(source, filepath: nil, line: nil, offset: nil, encoding: nil, frozen_string_literal: nil, verbose: nil, scopes: nil); end
34+
sig { params(source: String, command_line: T.nilable(String), encoding: T.nilable(T.any(String, Encoding)), filepath: T.nilable(String), frozen_string_literal: T.nilable(T::Boolean), line: T.nilable(Integer), scopes: T.nilable(T::Array[T::Array[Symbol]]), version: T.nilable(String)).returns(T::Array[Prism::Comment]) }
35+
def self.parse_comments(source, command_line: nil, encoding: nil, filepath: nil, frozen_string_literal: nil, line: nil, scopes: nil, version: nil); end
3636

37-
sig { params(filepath: String, line: T.nilable(Integer), offset: T.nilable(Integer), encoding: T.nilable(Encoding), frozen_string_literal: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean), scopes: T.nilable(T::Array[T::Array[Symbol]])).returns(T::Array[Prism::Comment]) }
38-
def self.parse_file_comments(filepath, line: nil, offset: nil, encoding: nil, frozen_string_literal: nil, verbose: nil, scopes: nil); end
37+
sig { params(filepath: String, command_line: T.nilable(String), encoding: T.nilable(T.any(String, Encoding)), frozen_string_literal: T.nilable(T::Boolean), line: T.nilable(Integer), scopes: T.nilable(T::Array[T::Array[Symbol]]), version: T.nilable(String)).returns(T::Array[Prism::Comment]) }
38+
def self.parse_file_comments(filepath, command_line: nil, encoding: nil, frozen_string_literal: nil, line: nil, scopes: nil, version: nil); end
3939

40-
sig { params(source: String, filepath: T.nilable(String), line: T.nilable(Integer), offset: T.nilable(Integer), encoding: T.nilable(Encoding), frozen_string_literal: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean), scopes: T.nilable(T::Array[T::Array[Symbol]])).returns(Prism::ParseResult[[Prism::ProgramNode, T::Array[T.untyped]]]) }
41-
def self.parse_lex(source, filepath: nil, line: nil, offset: nil, encoding: nil, frozen_string_literal: nil, verbose: nil, scopes: nil); end
40+
sig { params(source: String, command_line: T.nilable(String), encoding: T.nilable(T.any(String, Encoding)), filepath: T.nilable(String), frozen_string_literal: T.nilable(T::Boolean), line: T.nilable(Integer), scopes: T.nilable(T::Array[T::Array[Symbol]]), version: T.nilable(String)).returns(Prism::ParseResult[[Prism::ProgramNode, T::Array[T.untyped]]]) }
41+
def self.parse_lex(source, command_line: nil, encoding: nil, filepath: nil, frozen_string_literal: nil, line: nil, scopes: nil, version: nil); end
4242

43-
sig { params(filepath: String, line: T.nilable(Integer), offset: T.nilable(Integer), encoding: T.nilable(Encoding), frozen_string_literal: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean), scopes: T.nilable(T::Array[T::Array[Symbol]])).returns(Prism::ParseResult[[Prism::ProgramNode, T::Array[T.untyped]]]) }
44-
def self.parse_lex_file(filepath, line: nil, offset: nil, encoding: nil, frozen_string_literal: nil, verbose: nil, scopes: nil); end
43+
sig { params(filepath: String, command_line: T.nilable(String), encoding: T.nilable(T.any(String, Encoding)), frozen_string_literal: T.nilable(T::Boolean), line: T.nilable(Integer), scopes: T.nilable(T::Array[T::Array[Symbol]]), version: T.nilable(String)).returns(Prism::ParseResult[[Prism::ProgramNode, T::Array[T.untyped]]]) }
44+
def self.parse_lex_file(filepath, command_line: nil, encoding: nil, frozen_string_literal: nil, line: nil, scopes: nil, version: nil); end
4545

46-
sig { params(source: String, filepath: T.nilable(String), line: T.nilable(Integer), offset: T.nilable(Integer), encoding: T.nilable(Encoding), frozen_string_literal: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean), scopes: T.nilable(T::Array[T::Array[Symbol]])).returns(T::Boolean) }
47-
def self.parse_success?(source, filepath: nil, line: nil, offset: nil, encoding: nil, frozen_string_literal: nil, verbose: nil, scopes: nil); end
46+
sig { params(source: String, command_line: T.nilable(String), encoding: T.nilable(T.any(String, Encoding)), filepath: T.nilable(String), frozen_string_literal: T.nilable(T::Boolean), line: T.nilable(Integer), scopes: T.nilable(T::Array[T::Array[Symbol]]), version: T.nilable(String)).returns(T::Boolean) }
47+
def self.parse_success?(source, command_line: nil, encoding: nil, filepath: nil, frozen_string_literal: nil, line: nil, scopes: nil, version: nil); end
4848

49-
sig { params(source: String, filepath: T.nilable(String), line: T.nilable(Integer), offset: T.nilable(Integer), encoding: T.nilable(Encoding), frozen_string_literal: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean), scopes: T.nilable(T::Array[T::Array[Symbol]])).returns(T::Boolean) }
50-
def self.parse_failure?(source, filepath: nil, line: nil, offset: nil, encoding: nil, frozen_string_literal: nil, verbose: nil, scopes: nil); end
49+
sig { params(source: String, command_line: T.nilable(String), encoding: T.nilable(T.any(String, Encoding)), filepath: T.nilable(String), frozen_string_literal: T.nilable(T::Boolean), line: T.nilable(Integer), scopes: T.nilable(T::Array[T::Array[Symbol]]), version: T.nilable(String)).returns(T::Boolean) }
50+
def self.parse_failure?(source, command_line: nil, encoding: nil, filepath: nil, frozen_string_literal: nil, line: nil, scopes: nil, version: nil); end
5151

52-
sig { params(filepath: String, line: T.nilable(Integer), offset: T.nilable(Integer), encoding: T.nilable(Encoding), frozen_string_literal: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean), scopes: T.nilable(T::Array[T::Array[Symbol]])).returns(T::Boolean) }
53-
def self.parse_file_success?(filepath, line: nil, offset: nil, encoding: nil, frozen_string_literal: nil, verbose: nil, scopes: nil); end
52+
sig { params(filepath: String, command_line: T.nilable(String), encoding: T.nilable(T.any(String, Encoding)), frozen_string_literal: T.nilable(T::Boolean), line: T.nilable(Integer), scopes: T.nilable(T::Array[T::Array[Symbol]]), version: T.nilable(String)).returns(T::Boolean) }
53+
def self.parse_file_success?(filepath, command_line: nil, encoding: nil, frozen_string_literal: nil, line: nil, scopes: nil, version: nil); end
5454

55-
sig { params(filepath: String, line: T.nilable(Integer), offset: T.nilable(Integer), encoding: T.nilable(Encoding), frozen_string_literal: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean), scopes: T.nilable(T::Array[T::Array[Symbol]])).returns(T::Boolean) }
56-
def self.parse_file_failure?(filepath, line: nil, offset: nil, encoding: nil, frozen_string_literal: nil, verbose: nil, scopes: nil); end
55+
sig { params(filepath: String, command_line: T.nilable(String), encoding: T.nilable(T.any(String, Encoding)), frozen_string_literal: T.nilable(T::Boolean), line: T.nilable(Integer), scopes: T.nilable(T::Array[T::Array[Symbol]]), version: T.nilable(String)).returns(T::Boolean) }
56+
def self.parse_file_failure?(filepath, command_line: nil, encoding: nil, frozen_string_literal: nil, line: nil, scopes: nil, version: nil); end
5757
end

0 commit comments

Comments
 (0)