Skip to content

Commit 17194e0

Browse files
committed
Split parse result based on type
1 parent cf3a6ff commit 17194e0

File tree

17 files changed

+226
-103
lines changed

17 files changed

+226
-103
lines changed

.github/dependabot.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ updates:
4040
directory: '/gemfiles/truffleruby'
4141
schedule:
4242
interval: 'weekly'
43-
# - package-ecosystem: 'bundler'
44-
# directory: '/gemfiles/typecheck'
45-
# schedule:
46-
# interval: 'weekly'
43+
- package-ecosystem: 'bundler'
44+
directory: '/gemfiles/typecheck'
45+
schedule:
46+
interval: 'weekly'

.github/workflows/main.yml

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,23 @@ jobs:
3838
env:
3939
LANG: "C"
4040

41-
# typecheck:
42-
# runs-on: ubuntu-latest
43-
# env:
44-
# BUNDLE_GEMFILE: gemfiles/typecheck/Gemfile
45-
# steps:
46-
# - uses: actions/checkout@v4
47-
# - name: Set up Ruby
48-
# uses: ruby/setup-ruby@v1
49-
# with:
50-
# ruby-version: "3.3"
51-
# bundler-cache: true
52-
# - name: Check Sorbet
53-
# run: bundle exec rake typecheck:tapioca typecheck:sorbet
54-
# - name: Check Steep
55-
# run: bundle exec rake typecheck:steep
56-
# - name: Check field kinds
57-
# run: rm lib/prism/node.rb && CHECK_FIELD_KIND=true bundle exec rake
41+
typecheck:
42+
runs-on: ubuntu-latest
43+
env:
44+
BUNDLE_GEMFILE: gemfiles/typecheck/Gemfile
45+
steps:
46+
- uses: actions/checkout@v4
47+
- name: Set up Ruby
48+
uses: ruby/setup-ruby@v1
49+
with:
50+
ruby-version: "3.3"
51+
bundler-cache: true
52+
# - name: Check Sorbet
53+
# run: bundle exec rake typecheck:tapioca typecheck:sorbet
54+
- name: Check Steep
55+
run: bundle exec rake typecheck:steep
56+
- name: Check field kinds
57+
run: rm lib/prism/node.rb && CHECK_FIELD_KIND=true bundle exec rake
5858

5959
build:
6060
strategy:

ext/prism/extension.c

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ VALUE rb_cPrismEmbDocComment;
1919
VALUE rb_cPrismMagicComment;
2020
VALUE rb_cPrismParseError;
2121
VALUE rb_cPrismParseWarning;
22+
VALUE rb_cPrismResult;
2223
VALUE rb_cPrismParseResult;
24+
VALUE rb_cPrismParseLexResult;
2325

2426
VALUE rb_cPrismDebugEncoding;
2527

@@ -515,7 +517,7 @@ parser_warnings(pm_parser_t *parser, rb_encoding *encoding, VALUE source) {
515517
* Create a new parse result from the given parser, value, encoding, and source.
516518
*/
517519
static VALUE
518-
parse_result_create(pm_parser_t *parser, VALUE value, rb_encoding *encoding, VALUE source) {
520+
parse_result_create(VALUE class, pm_parser_t *parser, VALUE value, rb_encoding *encoding, VALUE source) {
519521
VALUE result_argv[] = {
520522
value,
521523
parser_comments(parser, source),
@@ -526,7 +528,7 @@ parse_result_create(pm_parser_t *parser, VALUE value, rb_encoding *encoding, VAL
526528
source
527529
};
528530

529-
return rb_class_new_instance(7, result_argv, rb_cPrismParseResult);
531+
return rb_class_new_instance(7, result_argv, class);
530532
}
531533

532534
/******************************************************************************/
@@ -635,7 +637,7 @@ parse_lex_input(pm_string_t *input, const pm_options_t *options, bool return_nod
635637
value = parse_lex_data.tokens;
636638
}
637639

638-
VALUE result = parse_result_create(&parser, value, parse_lex_data.encoding, source);
640+
VALUE result = parse_result_create(rb_cPrismParseLexResult, &parser, value, parse_lex_data.encoding, source);
639641
pm_node_destroy(&parser, node);
640642
pm_parser_free(&parser);
641643

@@ -700,7 +702,7 @@ parse_input(pm_string_t *input, const pm_options_t *options) {
700702

701703
VALUE source = pm_source_new(&parser, encoding);
702704
VALUE value = pm_ast_new(&parser, node, encoding, source);
703-
VALUE result = parse_result_create(&parser, value, encoding, source) ;
705+
VALUE result = parse_result_create(rb_cPrismParseResult, &parser, value, encoding, source) ;
704706

705707
pm_node_destroy(&parser, node);
706708
pm_parser_free(&parser);
@@ -804,7 +806,7 @@ parse_stream(int argc, VALUE *argv, VALUE self) {
804806

805807
VALUE source = pm_source_new(&parser, encoding);
806808
VALUE value = pm_ast_new(&parser, node, encoding, source);
807-
VALUE result = parse_result_create(&parser, value, encoding, source);
809+
VALUE result = parse_result_create(rb_cPrismParseResult, &parser, value, encoding, source);
808810

809811
pm_node_destroy(&parser, node);
810812
pm_buffer_free(&buffer);
@@ -1362,7 +1364,10 @@ Init_prism(void) {
13621364
rb_cPrismMagicComment = rb_define_class_under(rb_cPrism, "MagicComment", rb_cObject);
13631365
rb_cPrismParseError = rb_define_class_under(rb_cPrism, "ParseError", rb_cObject);
13641366
rb_cPrismParseWarning = rb_define_class_under(rb_cPrism, "ParseWarning", rb_cObject);
1365-
rb_cPrismParseResult = rb_define_class_under(rb_cPrism, "ParseResult", rb_cObject);
1367+
1368+
rb_cPrismResult = rb_define_class_under(rb_cPrism, "Result", rb_cObject);
1369+
rb_cPrismParseResult = rb_define_class_under(rb_cPrism, "ParseResult", rb_cPrismResult);
1370+
rb_cPrismParseLexResult = rb_define_class_under(rb_cPrism, "ParseLexResult", rb_cPrismResult);
13661371

13671372
// Intern all of the options that we support so that we don't have to do it
13681373
// every time we parse.

gemfiles/typecheck/Gemfile.lock

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ GEM
2929
fileutils (1.7.2)
3030
i18n (1.14.4)
3131
concurrent-ruby (~> 1.0)
32-
json (2.7.1)
32+
json (2.7.2)
3333
language_server-protocol (3.17.0.3)
3434
listen (3.9.0)
3535
rb-fsevent (~> 0.10, >= 0.10.3)
@@ -51,8 +51,8 @@ GEM
5151
rb-fsevent (0.11.2)
5252
rb-inotify (0.10.1)
5353
ffi (~> 1.0)
54-
rbi (0.1.10)
55-
prism (>= 0.18.0, < 0.25)
54+
rbi (0.1.11)
55+
prism (>= 0.18.0, < 0.27)
5656
sorbet-runtime (>= 0.5.9204)
5757
rbs (3.4.4)
5858
abbrev
@@ -61,15 +61,15 @@ GEM
6161
sexp_processor (~> 4.16)
6262
securerandom (0.3.1)
6363
sexp_processor (4.17.1)
64-
sorbet (0.5.11319)
65-
sorbet-static (= 0.5.11319)
66-
sorbet-runtime (0.5.11319)
67-
sorbet-static (0.5.11319-aarch64-linux)
68-
sorbet-static (0.5.11319-universal-darwin)
69-
sorbet-static (0.5.11319-x86_64-linux)
70-
sorbet-static-and-runtime (0.5.11319)
71-
sorbet (= 0.5.11319)
72-
sorbet-runtime (= 0.5.11319)
64+
sorbet (0.5.11351)
65+
sorbet-static (= 0.5.11351)
66+
sorbet-runtime (0.5.11351)
67+
sorbet-static (0.5.11351-aarch64-linux)
68+
sorbet-static (0.5.11351-universal-darwin)
69+
sorbet-static (0.5.11351-x86_64-linux)
70+
sorbet-static-and-runtime (0.5.11351)
71+
sorbet (= 0.5.11351)
72+
sorbet-runtime (= 0.5.11351)
7373
spoom (1.3.0)
7474
erubi (>= 1.10.0)
7575
prism (>= 0.19.0)
@@ -91,7 +91,7 @@ GEM
9191
strscan (>= 1.0.0)
9292
terminal-table (>= 2, < 4)
9393
strscan (3.1.0)
94-
tapioca (0.13.1)
94+
tapioca (0.13.3)
9595
bundler (>= 2.2.25)
9696
netrc (>= 0.11.0)
9797
parallel (>= 1.21.0)

lib/prism.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ module Prism
3737
private_constant :LexRipper
3838

3939
# :call-seq:
40-
# Prism::lex_compat(source, **options) -> ParseResult
40+
# Prism::lex_compat(source, **options) -> LexCompat::Result
4141
#
4242
# Returns a parse result whose value is an array of tokens that closely
4343
# resembles the return value of Ripper::lex. The main difference is that the

lib/prism/ffi.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ def parse_lex_common(string, code, options) # :nodoc:
350350
node, comments, magic_comments, data_loc, errors, warnings = loader.load_nodes
351351
tokens.each { |token,| token.value.force_encoding(loader.encoding) }
352352

353-
ParseResult.new([node, tokens], comments, magic_comments, data_loc, errors, warnings, source)
353+
ParseLexResult.new([node, tokens], comments, magic_comments, data_loc, errors, warnings, source)
354354
end
355355
end
356356

lib/prism/lex_compat.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,23 @@ module Prism
1010
# generally lines up. However, there are a few cases that require special
1111
# handling.
1212
class LexCompat # :nodoc:
13+
# A result class specialized for holding tokens produced by the lexer.
14+
class Result < Prism::Result
15+
# The list of tokens that were produced by the lexer.
16+
attr_reader :value
17+
18+
# Create a new lex compat result object with the given values.
19+
def initialize(value, comments, magic_comments, data_loc, errors, warnings, source)
20+
@value = value
21+
super(comments, magic_comments, data_loc, errors, warnings, source)
22+
end
23+
24+
# Implement the hash pattern matching interface for Result.
25+
def deconstruct_keys(keys)
26+
super.merge!(value: value)
27+
end
28+
end
29+
1330
# This is a mapping of prism token types to Ripper token types. This is a
1431
# many-to-one mapping because we split up our token types, whereas Ripper
1532
# tends to group them.
@@ -844,7 +861,7 @@ def result
844861
# We sort by location to compare against Ripper's output
845862
tokens.sort_by!(&:location)
846863

847-
ParseResult.new(tokens, result.comments, result.magic_comments, result.data_loc, result.errors, result.warnings, Source.new(source))
864+
Result.new(tokens, result.comments, result.magic_comments, result.data_loc, result.errors, result.warnings, Source.new(source))
848865
end
849866
end
850867

lib/prism/parse_result.rb

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -438,14 +438,9 @@ def inspect
438438
end
439439

440440
# This represents the result of a call to ::parse or ::parse_file. It contains
441-
# the AST, any comments that were encounters, and any errors that were
442-
# encountered.
443-
class ParseResult
444-
# The value that was generated by parsing. Normally this holds the AST, but
445-
# it can sometimes how a list of tokens or other results passed back from
446-
# the parser.
447-
attr_reader :value
448-
441+
# the requested structure, any comments that were encounters, and any errors
442+
# that were encountered.
443+
class Result
449444
# The list of comments that were encountered during parsing.
450445
attr_reader :comments
451446

@@ -466,9 +461,8 @@ class ParseResult
466461
# A Source instance that represents the source code that was parsed.
467462
attr_reader :source
468463

469-
# Create a new parse result object with the given values.
470-
def initialize(value, comments, magic_comments, data_loc, errors, warnings, source)
471-
@value = value
464+
# Create a new result object with the given values.
465+
def initialize(comments, magic_comments, data_loc, errors, warnings, source)
472466
@comments = comments
473467
@magic_comments = magic_comments
474468
@data_loc = data_loc
@@ -477,9 +471,9 @@ def initialize(value, comments, magic_comments, data_loc, errors, warnings, sour
477471
@source = source
478472
end
479473

480-
# Implement the hash pattern matching interface for ParseResult.
474+
# Implement the hash pattern matching interface for Result.
481475
def deconstruct_keys(keys)
482-
{ value: value, comments: comments, magic_comments: magic_comments, data_loc: data_loc, errors: errors, warnings: warnings }
476+
{ comments: comments, magic_comments: magic_comments, data_loc: data_loc, errors: errors, warnings: warnings }
483477
end
484478

485479
# Returns the encoding of the source code that was parsed.
@@ -500,6 +494,58 @@ def failure?
500494
end
501495
end
502496

497+
# This is a result specific to the `parse` and `parse_file` methods.
498+
class ParseResult < Result
499+
# The syntax tree that was parsed from the source code.
500+
attr_reader :value
501+
502+
# Create a new parse result object with the given values.
503+
def initialize(value, comments, magic_comments, data_loc, errors, warnings, source)
504+
@value = value
505+
super(comments, magic_comments, data_loc, errors, warnings, source)
506+
end
507+
508+
# Implement the hash pattern matching interface for ParseResult.
509+
def deconstruct_keys(keys)
510+
super.merge!(value: value)
511+
end
512+
end
513+
514+
# This is a result specific to the `lex` and `lex_file` methods.
515+
class LexResult < Result
516+
# The list of tokens that were parsed from the source code.
517+
attr_reader :value
518+
519+
# Create a new lex result object with the given values.
520+
def initialize(value, comments, magic_comments, data_loc, errors, warnings, source)
521+
@value = value
522+
super(comments, magic_comments, data_loc, errors, warnings, source)
523+
end
524+
525+
# Implement the hash pattern matching interface for LexResult.
526+
def deconstruct_keys(keys)
527+
super.merge!(value: value)
528+
end
529+
end
530+
531+
# This is a result specific to the `parse_lex` and `parse_lex_file` methods.
532+
class ParseLexResult < Result
533+
# A tuple of the syntax tree and the list of tokens that were parsed from
534+
# the source code.
535+
attr_reader :value
536+
537+
# Create a new parse lex result object with the given values.
538+
def initialize(value, comments, magic_comments, data_loc, errors, warnings, source)
539+
@value = value
540+
super(comments, magic_comments, data_loc, errors, warnings, source)
541+
end
542+
543+
# Implement the hash pattern matching interface for ParseLexResult.
544+
def deconstruct_keys(keys)
545+
super.merge!(value: value)
546+
end
547+
end
548+
503549
# This represents a token from the Ruby source.
504550
class Token
505551
# The Source object that represents the source this token came from.

lib/prism/parse_result/newlines.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,6 @@ def visit_statements_node(node)
5858

5959
# Walk the tree and mark nodes that are on a new line.
6060
def mark_newlines!
61-
value = self.value
62-
raise "This method should only be called on a parse result that contains a node" unless Node === value
6361
value.accept(Newlines.new(Array.new(1 + source.offsets.size, false))) # steep:ignore
6462
end
6563
end

rbi/prism.rbi

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,28 @@ module Prism
77
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) }
88
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, 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]]) }
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::LexResult) }
1111
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, 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]]) }
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::LexResult) }
1414
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, options: T::Hash[Symbol, T.untyped]).returns(Prism::ParseResult[T::Array[T.untyped]]) }
16+
sig { params(source: String, options: T::Hash[Symbol, T.untyped]).returns(Prism::LexCompat::Result) }
1717
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

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

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]) }
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) }
2626
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, 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]) }
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) }
2929
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), 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]) }
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) }
3232
def self.parse_stream(stream, command_line: nil, encoding: nil, filepath: nil, frozen_string_literal: nil, line: nil, scopes: nil, version: nil); end
3333

3434
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]) }
@@ -37,10 +37,10 @@ module Prism
3737
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]) }
3838
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, 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]]]) }
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::ParseLexResult) }
4141
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, 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]]]) }
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::ParseLexResult) }
4444
def self.parse_lex_file(filepath, command_line: nil, encoding: nil, frozen_string_literal: nil, line: nil, scopes: nil, version: nil); end
4545

4646
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) }

0 commit comments

Comments
 (0)