codependency 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. data/Guardfile +4 -4
  2. data/README.md +7 -4
  3. data/codependency.gemspec +6 -4
  4. data/lib/codependency.rb +1 -0
  5. data/lib/codependency/graph.rb +27 -34
  6. data/lib/codependency/parser.rb +11 -15
  7. data/lib/codependency/path.rb +53 -0
  8. data/lib/codependency/version.rb +1 -1
  9. data/spec/codependency/graph_spec.rb +89 -51
  10. data/spec/codependency/graph_spec/codependency_graph/files/assets/application.txt +6 -0
  11. data/spec/codependency/graph_spec/codependency_graph/files/assets/templates_account.txt +3 -0
  12. data/spec/codependency/graph_spec/codependency_graph/files/assets/templates_history.txt +3 -0
  13. data/spec/codependency/graph_spec/codependency_graph/files/assets/templates_user.txt +5 -0
  14. data/spec/codependency/graph_spec/codependency_graph/files/breakfast/butter.txt +3 -0
  15. data/spec/codependency/graph_spec/codependency_graph/files/breakfast/egg.txt +4 -0
  16. data/spec/codependency/graph_spec/codependency_graph/files/breakfast/sandwich.txt +6 -0
  17. data/spec/codependency/graph_spec/codependency_graph/files/breakfast/toast.txt +4 -0
  18. data/spec/codependency/graph_spec/codependency_graph/files/solar_system/body.txt +3 -0
  19. data/spec/codependency/graph_spec/codependency_graph/files/solar_system/earth.txt +5 -0
  20. data/spec/codependency/graph_spec/codependency_graph/files/solar_system/mars.txt +5 -0
  21. data/spec/codependency/graph_spec/codependency_graph/files/solar_system/phobos.txt +6 -0
  22. data/spec/codependency/graph_spec/codependency_graph/files/solar_system/planet.txt +4 -0
  23. data/spec/codependency/graph_spec/codependency_graph/require/assets/application.txt +11 -0
  24. data/spec/codependency/graph_spec/codependency_graph/require/assets/templates_account.txt +3 -0
  25. data/spec/codependency/graph_spec/codependency_graph/require/assets/templates_history.txt +3 -0
  26. data/spec/codependency/graph_spec/codependency_graph/require/assets/templates_user.txt +8 -0
  27. data/spec/codependency/graph_spec/codependency_graph/require/breakfast/butter.txt +3 -0
  28. data/spec/codependency/graph_spec/codependency_graph/require/breakfast/egg.txt +6 -0
  29. data/spec/codependency/graph_spec/codependency_graph/require/breakfast/sandwich.txt +13 -0
  30. data/spec/codependency/graph_spec/codependency_graph/require/breakfast/toast.txt +6 -0
  31. data/spec/codependency/graph_spec/codependency_graph/require/lox/money.txt +11 -0
  32. data/spec/codependency/graph_spec/codependency_graph/require/lox/power.txt +11 -0
  33. data/spec/codependency/graph_spec/codependency_graph/require/lox/respect.txt +11 -0
  34. data/spec/codependency/graph_spec/codependency_graph/require/solar_system/body.txt +3 -0
  35. data/spec/codependency/graph_spec/codependency_graph/require/solar_system/earth.txt +9 -0
  36. data/spec/codependency/graph_spec/codependency_graph/require/solar_system/mars.txt +9 -0
  37. data/spec/codependency/graph_spec/codependency_graph/require/solar_system/phobos.txt +13 -0
  38. data/spec/codependency/graph_spec/codependency_graph/require/solar_system/planet.txt +6 -0
  39. data/spec/codependency/graph_spec/codependency_graph/tsort/assets/application.txt +6 -0
  40. data/spec/codependency/graph_spec/codependency_graph/tsort/assets/templates_account.txt +3 -0
  41. data/spec/codependency/graph_spec/codependency_graph/tsort/assets/templates_history.txt +3 -0
  42. data/spec/codependency/graph_spec/codependency_graph/tsort/assets/templates_user.txt +5 -0
  43. data/spec/codependency/graph_spec/codependency_graph/tsort/breakfast/butter.txt +3 -0
  44. data/spec/codependency/graph_spec/codependency_graph/tsort/breakfast/egg.txt +4 -0
  45. data/spec/codependency/graph_spec/codependency_graph/tsort/breakfast/sandwich.txt +6 -0
  46. data/spec/codependency/graph_spec/codependency_graph/tsort/breakfast/toast.txt +4 -0
  47. data/spec/codependency/graph_spec/codependency_graph/tsort/solar_system/body.txt +3 -0
  48. data/spec/codependency/graph_spec/codependency_graph/tsort/solar_system/earth.txt +5 -0
  49. data/spec/codependency/graph_spec/codependency_graph/tsort/solar_system/mars.txt +5 -0
  50. data/spec/codependency/graph_spec/codependency_graph/tsort/solar_system/phobos.txt +6 -0
  51. data/spec/codependency/graph_spec/codependency_graph/tsort/solar_system/planet.txt +4 -0
  52. data/spec/codependency/parser_spec.rb +48 -40
  53. data/spec/codependency/path_spec.rb +55 -0
  54. data/spec/fixtures/assets/application.js +1 -0
  55. data/spec/fixtures/assets/templates/account.js +1 -0
  56. data/spec/fixtures/assets/templates/history.js +1 -0
  57. data/spec/fixtures/assets/templates/user.js +4 -0
  58. data/spec/fixtures/breakfast/butter.js +1 -0
  59. data/spec/fixtures/breakfast/egg.js +3 -0
  60. data/spec/fixtures/breakfast/sandwich.js +4 -0
  61. data/spec/fixtures/breakfast/toast.js +3 -0
  62. data/spec/fixtures/lox/money.rb +4 -0
  63. data/spec/fixtures/lox/power.rb +4 -0
  64. data/spec/fixtures/lox/respect.rb +4 -0
  65. data/spec/fixtures/solar_system/body.rb +2 -0
  66. data/spec/fixtures/solar_system/earth.rb +4 -0
  67. data/spec/fixtures/solar_system/mars.rb +4 -0
  68. data/spec/fixtures/solar_system/phobos.rb +5 -0
  69. data/spec/fixtures/solar_system/planet.rb +4 -0
  70. data/spec/spec_helper.rb +5 -2
  71. data/spec/support/approvals.rb +1 -0
  72. metadata +146 -17
  73. data/spec/support/breakfasts_context.rb +0 -24
  74. data/spec/support/circular_context.rb +0 -25
  75. data/spec/support/file_system_stubs.rb +0 -10
  76. data/spec/support/planets_context.rb +0 -38
  77. data/spec/support/subdirectories_context.rb +0 -21
data/Guardfile CHANGED
@@ -1,6 +1,6 @@
1
1
  guard 'rspec' do
2
- watch(%r{^spec/.+_spec\.rb$})
3
- watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
- watch('spec/spec_helper.rb') { 'spec' }
5
- watch(%r{spec/support/.+\.rb}){ 'spec' }
2
+ watch( %r{^spec/.+_spec\.rb$} )
3
+ watch( %r{^lib/(.+)\.rb$} ){ |m| "spec/#{m[1]}_spec.rb" }
4
+ watch( 'spec/spec_helper.rb' ){ 'spec' }
5
+ watch( %r{spec/support/.+\.rb} ){ 'spec' }
6
6
  end
data/README.md CHANGED
@@ -18,12 +18,14 @@ Or install it yourself as:
18
18
 
19
19
  ## Usage
20
20
 
21
- Say you have two files, `bar.rb` and `foo.rb`. `bar` comes before `foo` in a natural naming scheme, but `bar` has a dependency on `foo`. We can express this using a simple comment syntax at the head of the file like this:
21
+ Say you have two files, `bar.rb` and `foo.rb`. `bar` comes before `foo` in a
22
+ natural naming scheme, but `bar` has a dependency on `foo`. We can express
23
+ this using a simple comment syntax at the head of the file like this:
22
24
 
23
25
  **bar.rb:**
24
26
 
25
27
  ``` rb
26
- # require foo
28
+ #= require foo
27
29
 
28
30
  class Bar
29
31
  end
@@ -39,8 +41,9 @@ end
39
41
  Then, we create a dependency graph to determine the order in which the files might need to be loaded, inserted, or compiled:
40
42
 
41
43
  ``` rb
42
- graph = Codependency::Graph.new "bar.rb"
43
- graph.files # => ["./foo.rb", "./bar.rb"]
44
+ graph = Codependency::Graph.new
45
+ graph.path << '.' # works like PATH, append search paths for this graph
46
+ graph.files # => ["./foo.rb", "./bar.rb"] # returns a topologically sorted list of relative filepaths
44
47
  ```
45
48
 
46
49
  ## Contributing
@@ -6,7 +6,8 @@ Gem::Specification.new do |gem|
6
6
  gem.email = ["jeremy.ruppel@gmail.com"]
7
7
  gem.description = %q{A pure ruby, comment-based dependency graph}
8
8
  gem.summary = %q{A pure ruby, comment-based dependency graph}
9
- gem.homepage = ""
9
+ gem.homepage = 'https://github.com/jeremyruppel/codependency'
10
+ gem.license = 'MIT'
10
11
 
11
12
  gem.files = `git ls-files`.split($\)
12
13
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
@@ -15,7 +16,8 @@ Gem::Specification.new do |gem|
15
16
  gem.require_paths = ["lib"]
16
17
  gem.version = Codependency::VERSION
17
18
 
18
- gem.add_development_dependency 'rspec', '2.11.0'
19
- gem.add_development_dependency 'guard-rspec', '1.2.1'
20
- gem.add_development_dependency 'rb-fsevent', '0.9.1'
19
+ gem.add_development_dependency 'rspec', '2.12.0'
20
+ gem.add_development_dependency 'guard-rspec', '1.2.1'
21
+ gem.add_development_dependency 'rb-fsevent', '0.9.1'
22
+ gem.add_development_dependency 'git-approvals', '0.2.0'
21
23
  end
@@ -3,4 +3,5 @@ require 'codependency/version'
3
3
  module Codependency
4
4
  autoload :Graph, 'codependency/graph'
5
5
  autoload :Parser, 'codependency/parser'
6
+ autoload :Path, 'codependency/path'
6
7
  end
@@ -2,59 +2,52 @@ require 'tsort'
2
2
 
3
3
  module Codependency
4
4
  class Graph < Hash
5
- def initialize( path, options={} )
6
- @path, @options = path, options
7
-
8
- super( ){ |h, k| h[ k ] = parser.parse( k ) }
9
- end
10
- attr_reader :path, :options
11
-
12
- include TSort
13
5
 
14
6
  ##
15
- # the dirname to use for this graph, based on the path
16
- def dirname
17
- File.dirname path
18
- end
19
-
20
- ##
21
- # the extname to use for this graph, based on the path
22
- def extname
23
- File.extname path
7
+ # Adds a file to the dependency graph. The filename given should be
8
+ # relative and should not contain an extension, like:
9
+ #
10
+ # # require application/user
11
+ #
12
+ # Attempts to locate and add all dependencies of this file recursively,
13
+ # creating our graph.
14
+ def require( file )
15
+ self[ file ] ||= parser.parse path[ file ]
16
+ self[ file ].each do |file|
17
+ self << file unless key?( file )
18
+ end
24
19
  end
20
+ alias :<< :require
25
21
 
26
22
  ##
27
- # walk the entire graph and return self
28
- def populate
29
- walk path
30
- self
23
+ # Returns the sorted list of files as determined by this graph,
24
+ # relative to the calling file.
25
+ def files
26
+ here = Pathname( caller.first.split( ':' ).shift )
27
+ tsort.map { |file| path[ file ].relative_path_from( here ) }
31
28
  end
32
29
 
33
30
  ##
34
- # discover all nodes in this graph by walking it
35
- def walk( path )
36
- self[ path ].each { |path| walk( path ) unless has_key?( path ) }
31
+ # The path set for this dependency graph.
32
+ def path
33
+ @path ||= Path.new parser.extensions
37
34
  end
38
35
 
39
36
  ##
40
- # the parser to use for this graph. shared by all nodes.
37
+ # The file parser for this dependency graph.
41
38
  def parser
42
- @parser ||= begin
43
- Parser.new options.merge( :dirname => dirname, :extname => extname )
44
- end
45
- end
46
-
47
- ##
48
- # a topologically sorted list of all dependencies of the `start` file.
49
- def files
50
- populate.tsort
39
+ @parser ||= Parser.new
51
40
  end
52
41
 
53
42
  private
54
43
 
44
+ include TSort
45
+
46
+ ##
55
47
  # tsort interface
56
48
  alias :tsort_each_node :each_key
57
49
 
50
+ ##
58
51
  # tsort interface
59
52
  def tsort_each_child( node, &block )
60
53
  fetch( node ).each( &block )
@@ -1,28 +1,24 @@
1
1
  module Codependency
2
2
  class Parser
3
3
 
4
- def initialize( options={} )
5
- @options = options
6
- @comment = options.delete( :comment ) || '#'
7
- @dirname = options.delete( :dirname ) || '.'
8
- @extname = options.delete( :extname ) || '.rb'
9
- end
4
+ PATTERNS = Hash.new { |hash, key|
5
+ raise "Unknown extension '#{key}'. Known extensions are #{hash.keys.inspect}."
6
+ }
7
+ PATTERNS[ '.rb' ] = /^#= require ([\w\/]+)$/
8
+ PATTERNS[ '.js' ] = /^\/\/= require ([\w\/]+)$/
10
9
 
11
10
  ##
12
- # determines a file's dependencies based on the configured comment pattern.
11
+ # Determines a file's dependencies based on the file's extension.
13
12
  def parse( file )
13
+ pattern = PATTERNS[ File.extname( file ) ]
14
+
14
15
  IO.readlines( file ).take_while do |line|
15
16
  line =~ pattern
16
- end.map { |line| "#{@dirname}/#{line[ pattern, 1 ]}#{@extname}" }
17
+ end.map { |line| line[ pattern, 1 ] }
17
18
  end
18
19
 
19
- protected
20
-
21
- ##
22
- # the comment pattern to use.
23
- # TODO allow this to be more configurable
24
- def pattern
25
- @pattern ||= /^#{@comment} require (.+)$/
20
+ def extensions
21
+ PATTERNS.keys
26
22
  end
27
23
  end
28
24
  end
@@ -0,0 +1,53 @@
1
+ require 'pathname'
2
+
3
+ module Codependency
4
+ class Path < Array
5
+
6
+ def initialize( extensions=%w| .rb | )
7
+ @extensions = Array(extensions)
8
+ end
9
+ attr_reader :extensions
10
+
11
+ ##
12
+ # Appends a path to this path set. If the path exists, it
13
+ # will be expanded. Raises Errno::ENOENT if the path
14
+ # does not exist. Raises Errno::ENOTDIR if the path
15
+ # is not a directory.
16
+ def <<( str )
17
+ path = Pathname( str )
18
+
19
+ case
20
+ when !path.exist?
21
+ raise Errno::ENOENT, path.to_path
22
+ when !path.directory?
23
+ raise Errno::ENOTDIR, path.to_path
24
+ else
25
+ super path.expand_path
26
+ end
27
+ end
28
+
29
+ ##
30
+ # Sugar syntax for calling #find if given a string.
31
+ # Otherwise, behaves like normal array access.
32
+ def []( path_or_index )
33
+ if path_or_index.is_a? String
34
+ find path_or_index
35
+ else
36
+ super
37
+ end
38
+ end
39
+
40
+ ##
41
+ # Attempts to find the given file in this path set.
42
+ # Raises Errno::ENOENT if the file cannot be found.
43
+ def find( str )
44
+ super lambda { raise Errno::ENOENT, str } do |dir|
45
+ path = extensions.find do |ext|
46
+ file = dir.join str + ext
47
+ break file if file.exist?
48
+ end
49
+ break path if path
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,3 +1,3 @@
1
1
  module Codependency
2
- VERSION = '1.0.0'
2
+ VERSION = '2.0.0'
3
3
  end
@@ -1,74 +1,112 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Codependency::Graph do
4
- describe 'accessors' do
5
- subject { described_class.new 'planets/phobos.rb' }
6
- its( :path ){ should eq( 'planets/phobos.rb' ) }
7
- its( :dirname ){ should eq( 'planets' ) }
8
- its( :extname ){ should eq( '.rb' ) }
9
- its( :options ){ should eq( { } ) }
10
- end
4
+ it { should be_a( Hash ) }
11
5
 
12
- describe 'populate', :files => :planets do
13
- subject { described_class.new './phobos.rb' }
6
+ describe '#path' do
7
+ its( :path ){ should be_a( Codependency::Path ) }
8
+ its( :path ){ should be_empty }
9
+ end
14
10
 
15
- context 'initially' do
16
- it { should be_empty }
17
- end
11
+ describe '#require' do
12
+ let( :dirname ){ example.example_group.description }
13
+ let( :basename ){ example.description }
14
+ let( :file ){ File.join dirname, basename }
18
15
 
19
- context 'after calling populate' do
20
- before { subject.populate }
16
+ before { subject.path << './spec/fixtures' }
17
+ before { subject << file }
21
18
 
22
- it { should include( './phobos.rb' => [ './body.rb', './mars.rb' ] ) }
23
- it { should include( './mars.rb' => [ './planet.rb' ] ) }
24
- it { should include( './planet.rb' => [ './body.rb' ] ) }
25
- it { should include( './body.rb' => [ ] ) }
19
+ describe 'solar_system' do
20
+ example( 'body' ){ verify { subject } }
21
+ example( 'earth' ){ verify { subject } }
22
+ example( 'mars' ){ verify { subject } }
23
+ example( 'phobos' ){ verify { subject } }
24
+ example( 'planet' ){ verify { subject } }
26
25
  end
27
-
28
- it 'should return itself' do
29
- subject.populate.should == subject
26
+ describe 'breakfast' do
27
+ example( 'butter' ){ verify { subject } }
28
+ example( 'egg' ){ verify { subject } }
29
+ example( 'sandwich' ){ verify { subject } }
30
+ example( 'toast' ){ verify { subject } }
30
31
  end
31
- end
32
-
33
- context 'planets', :files => :planets do
34
- subject { described_class.new start }
35
-
36
- context 'earth' do
37
- let( :start ){ './earth.rb' }
38
- its( :files ){ should eq( %w| ./body.rb ./planet.rb ./earth.rb | ) }
32
+ describe 'lox' do
33
+ example( 'money' ){ verify { subject } }
34
+ example( 'power' ){ verify { subject } }
35
+ example( 'respect' ){ verify { subject } }
39
36
  end
40
-
41
- context 'phobos' do
42
- let( :start ){ './phobos.rb' }
43
- its( :files ){ should eq( %w| ./body.rb ./planet.rb ./mars.rb ./phobos.rb | ) }
37
+ describe 'assets' do
38
+ example( 'templates/account' ){ verify { subject } }
39
+ example( 'templates/history' ){ verify { subject } }
40
+ example( 'templates/user' ){ verify { subject } }
41
+ example( 'application' ){ verify { subject } }
44
42
  end
45
43
  end
46
44
 
47
- context 'breakfasts', :files => :breakfasts do
48
- subject { described_class.new start, :comment => '//' }
45
+ describe '#tsort' do
46
+ let( :dirname ){ example.example_group.description }
47
+ let( :basename ){ example.description }
48
+ let( :file ){ File.join dirname, basename }
49
+
50
+ before { subject.path << './spec/fixtures' }
51
+ before { subject << file }
49
52
 
50
- context 'sandwich' do
51
- let( :start ){ './sandwich.js' }
52
- its( :files ){ should eq( %w| ./butter.js ./egg.js ./toast.js ./sandwich.js | ) }
53
+ describe 'solar_system' do
54
+ example( 'body' ){ verify { subject.tsort } }
55
+ example( 'earth' ){ verify { subject.tsort } }
56
+ example( 'mars' ){ verify { subject.tsort } }
57
+ example( 'phobos' ){ verify { subject.tsort } }
58
+ example( 'planet' ){ verify { subject.tsort } }
59
+ end
60
+ describe 'breakfast' do
61
+ example( 'butter' ){ verify { subject.tsort } }
62
+ example( 'egg' ){ verify { subject.tsort } }
63
+ example( 'sandwich' ){ verify { subject.tsort } }
64
+ example( 'toast' ){ verify { subject.tsort } }
65
+ end
66
+ describe 'lox' do
67
+ example( 'money' ){ expect { subject.tsort }.to raise_error( TSort::Cyclic ) }
68
+ example( 'power' ){ expect { subject.tsort }.to raise_error( TSort::Cyclic ) }
69
+ example( 'respect' ){ expect { subject.tsort }.to raise_error( TSort::Cyclic ) }
70
+ end
71
+ describe 'assets' do
72
+ example( 'templates/account' ){ verify { subject.tsort } }
73
+ example( 'templates/history' ){ verify { subject.tsort } }
74
+ example( 'templates/user' ){ verify { subject.tsort } }
75
+ example( 'application' ){ verify { subject.tsort } }
53
76
  end
54
77
  end
55
78
 
56
- context 'circular dependencies', :files => :circular do
57
- subject { described_class.new start }
79
+ describe '#files' do
80
+ let( :dirname ){ example.example_group.description }
81
+ let( :basename ){ example.description }
82
+ let( :file ){ File.join dirname, basename }
58
83
 
59
- let( :start ){ './money.rb' }
84
+ before { subject.path << './spec/fixtures' }
85
+ before { subject << file }
60
86
 
61
- it 'should raise an error' do
62
- expect { subject.files }.to raise_error( TSort::Cyclic )
87
+ describe 'solar_system' do
88
+ example( 'body' ){ verify { subject.files } }
89
+ example( 'earth' ){ verify { subject.files } }
90
+ example( 'mars' ){ verify { subject.files } }
91
+ example( 'phobos' ){ verify { subject.files } }
92
+ example( 'planet' ){ verify { subject.files } }
63
93
  end
64
- end
65
-
66
- context 'subdirectories', :files => :subdirectories do
67
- subject { described_class.new start, :comment => '//' }
68
-
69
- context 'application.js' do
70
- let( :start ){ 'assets/application.js' }
71
- its( :files ){ should eq( %w| assets/templates/user/history.js assets/templates/user/account.js assets/templates/user.js assets/application.js | ) }
94
+ describe 'breakfast' do
95
+ example( 'butter' ){ verify { subject.files } }
96
+ example( 'egg' ){ verify { subject.files } }
97
+ example( 'sandwich' ){ verify { subject.files } }
98
+ example( 'toast' ){ verify { subject.files } }
99
+ end
100
+ describe 'lox' do
101
+ example( 'money' ){ expect { subject.tsort }.to raise_error( TSort::Cyclic ) }
102
+ example( 'power' ){ expect { subject.tsort }.to raise_error( TSort::Cyclic ) }
103
+ example( 'respect' ){ expect { subject.tsort }.to raise_error( TSort::Cyclic ) }
104
+ end
105
+ describe 'assets' do
106
+ example( 'templates/account' ){ verify { subject.files } }
107
+ example( 'templates/history' ){ verify { subject.files } }
108
+ example( 'templates/user' ){ verify { subject.files } }
109
+ example( 'application' ){ verify { subject.files } }
72
110
  end
73
111
  end
74
112
  end
@@ -0,0 +1,6 @@
1
+ [
2
+ [0] #<Pathname:../../fixtures/assets/templates/history.js>,
3
+ [1] #<Pathname:../../fixtures/assets/templates/account.js>,
4
+ [2] #<Pathname:../../fixtures/assets/templates/user.js>,
5
+ [3] #<Pathname:../../fixtures/assets/application.js>
6
+ ]
@@ -0,0 +1,3 @@
1
+ [
2
+ [0] #<Pathname:../../fixtures/assets/templates/account.js>
3
+ ]
@@ -0,0 +1,3 @@
1
+ [
2
+ [0] #<Pathname:../../fixtures/assets/templates/history.js>
3
+ ]
@@ -0,0 +1,5 @@
1
+ [
2
+ [0] #<Pathname:../../fixtures/assets/templates/history.js>,
3
+ [1] #<Pathname:../../fixtures/assets/templates/account.js>,
4
+ [2] #<Pathname:../../fixtures/assets/templates/user.js>
5
+ ]
@@ -0,0 +1,3 @@
1
+ [
2
+ [0] #<Pathname:../../fixtures/breakfast/butter.js>
3
+ ]