noraneko 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1fdc7d705f1b1f6bf4d1b2e18f35be6e8947401f
4
+ data.tar.gz: eeb55e85fd62462e20f3e3ca52f9af51127c995d
5
+ SHA512:
6
+ metadata.gz: 61fb7cded8e9e2bc442c0b80049ede733b727b1090c3a03eabecde54a83e0b9d91e7e09cdb53c82cc214d3024b4d1ea4b77c56c5384f6d85e703e0575565c182
7
+ data.tar.gz: afeb20537da8678b8983aa3091f82fbf516093cbaabc45c3db1a3772f8c0b789d2e0f8471d40932a09eb5f2536655152b7934baaa3b1ac32e7c26ea1491bd33c
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
13
+
14
+ # ctags
15
+ tags
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1 @@
1
+ 2.4.0
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.0
5
+ before_install: gem install bundler -v 1.15.3
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at rise.shia@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in noraneko.gemspec
8
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Shia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,35 @@
1
+ # Noraneko
2
+
3
+ Noraneko is aim to guess unused codes, especially in a Rails project.
4
+ This gem tries to find:
5
+
6
+ - The methods not to be called in the models, helpers, concerns and plain ruby classes.
7
+ - The dead views, meaning to say, which are not rendered.
8
+
9
+ Please be careful if you want to try this, it doesn't grant detected code is
10
+ deletable, but may be deletable. Because Ruby is hard to analyze in static
11
+ as you know. I will write some cases gem couldn't detect when it becomes stable.
12
+
13
+ ## Installation
14
+
15
+ ```ruby
16
+ gem install noraneko
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ noraneko path1,path2,path3 # if you don't pass, default path is '.'
23
+ ```
24
+
25
+ ## Contributing
26
+
27
+ Bug reports and pull requests are welcome on GitHub at https://github.com/riseshia/noraneko.
28
+
29
+ ## License
30
+
31
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
32
+
33
+ ## Code of Conduct
34
+
35
+ Everyone interacting in the Noraneko project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/riseshia/noraneko).
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'noraneko'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift("#{__dir__}/../lib")
5
+
6
+ require 'noraneko'
7
+
8
+ cli = Noraneko::CLI.new
9
+ result = cli.run
10
+
11
+ exit result
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'noraneko/cli'
4
+ require 'noraneko/node_utility'
5
+ require 'noraneko/nconst'
6
+ require 'noraneko/nclass'
7
+ require 'noraneko/nmodule'
8
+ require 'noraneko/nmethod'
9
+ require 'noraneko/nview'
10
+ require 'noraneko/processor'
11
+ require 'noraneko/project'
12
+ require 'noraneko/registry'
13
+ require 'noraneko/runner'
14
+ require 'noraneko/version'
15
+ require 'noraneko/view_processor'
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noraneko
4
+ class CLI
5
+ # @param args [Array<String>] command line arguments
6
+ # @return [Integer] UNIX exit code
7
+ def run(args = ARGV)
8
+ paths = args.empty? ? ['.'] : args.first.split(',')
9
+ execute_runner(paths)
10
+ rescue StandardError, SyntaxError => e
11
+ $stderr.puts e.message
12
+ $stderr.puts e.backtrace
13
+ return 2
14
+ end
15
+
16
+ private
17
+
18
+ def execute_runner(paths)
19
+ result = Runner.new.run(paths)
20
+ print_result result
21
+
22
+ result.empty? ? 0 : 1
23
+ end
24
+
25
+ def print_result(unuseds)
26
+ if unuseds.empty?
27
+ puts 'It seems that there is no unused method or modules'
28
+ else
29
+ unuseds.each do |unused|
30
+ puts "#{unused.loc} - #{unused.qualified_name} seem to be not used."
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noraneko
4
+ class NClass < ::Noraneko::NConst
5
+ end
6
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noraneko
4
+ class NConst
5
+ attr_accessor :included_module_names, :extended_module_names,
6
+ :registered_callbacks
7
+ attr_reader :qualified_name, :namespace, :path
8
+
9
+ def initialize(qualified_name, path, line)
10
+ @qualified_name = qualified_name
11
+ @namespace = qualified_name.split('::')
12
+ @path = path
13
+ @line = line
14
+ @methods = []
15
+ @included_module_names = []
16
+ @extended_module_names = []
17
+ @registered_callbacks = []
18
+ @called_views = []
19
+ @called_methods = []
20
+ @default_scope = :public
21
+ @default_type = :instance
22
+ end
23
+
24
+ def loc
25
+ "#{@path}:#{@line}"
26
+ end
27
+
28
+ def name
29
+ @namespace.last || ''
30
+ end
31
+
32
+ def parent_name
33
+ qualify(@namespace[0..-2])
34
+ end
35
+
36
+ def child_qualified_name(names)
37
+ qualify(@namespace + names)
38
+ end
39
+
40
+ def register_send(method_name, called_method_name)
41
+ method = find_method(method_name)
42
+ method.called_methods << called_method_name if method
43
+ end
44
+
45
+ def register_csend(called_method_name)
46
+ @called_methods << called_method_name
47
+ end
48
+
49
+ def find_method(method_name)
50
+ @methods.find do |method|
51
+ method.name == method_name
52
+ end
53
+ end
54
+
55
+ def private!
56
+ @default_scope = :private
57
+ end
58
+
59
+ def method_default_as_class!
60
+ @default_type = :class
61
+ end
62
+
63
+ def controller?
64
+ name.end_with?('Controller')
65
+ end
66
+
67
+ def all_methods
68
+ @methods
69
+ end
70
+
71
+ def all_instance_methods
72
+ @methods.select { |method| method.instance_method? }
73
+ end
74
+
75
+ def all_private_methods
76
+ @methods.select { |method| method.in_private? }
77
+ end
78
+
79
+ def all_public_methods
80
+ @methods.select { |method| method.in_public? }
81
+ end
82
+
83
+ def all_used_modules
84
+ @included_module_names + @extended_module_names
85
+ end
86
+
87
+ def add_method(name, line)
88
+ nmethod = NMethod.new(self, name, line, @default_scope, @default_type)
89
+ @methods << nmethod
90
+ nmethod
91
+ end
92
+
93
+ def add_cmethod(name, line)
94
+ nmethod = NMethod.class_method(self, name, line)
95
+ @methods << nmethod
96
+ nmethod
97
+ end
98
+
99
+ def called_view(view_name)
100
+ @called_views << view_name
101
+ end
102
+
103
+ def make_method_private(name)
104
+ target = @methods.find { |method| method.name == name }
105
+ target.private!
106
+ end
107
+
108
+ def merge_singleton(other)
109
+ cm = other.all_instance_methods
110
+ cm.each(&:class_method!)
111
+ @methods += cm
112
+ end
113
+
114
+ def called?(target_name)
115
+ @called_methods.any? { |name| name == target_name }
116
+ end
117
+
118
+ def used?(target_method)
119
+ return true if controller? && action_of_this?(target_method)
120
+ return true if registered_callback?(target_method.name)
121
+ all_methods.any? { |method| method.called?(target_method.name) }
122
+ end
123
+
124
+ def used_view?(target_view_name)
125
+ explicit = @called_views.any? { |name| name == target_view_name }
126
+ return true if explicit
127
+ return false unless target_view_name.start_with?(rel_path_from_controller)
128
+ tokens = target_view_name.split('/')
129
+ return false if tokens.size < 2
130
+ method_name = tokens.last.to_sym
131
+ all_public_methods.any? { |m| m.name == method_name }
132
+ end
133
+
134
+ def rel_path_from_controller
135
+ @path
136
+ .split('/controllers/').drop(1).join('')
137
+ .split('_controller.rb').first + '/'
138
+ end
139
+
140
+ private
141
+
142
+ def action_of_this?(target_method)
143
+ target_method.in?(self) && target_method.in_public?
144
+ end
145
+
146
+ def registered_callback?(method_name)
147
+ @registered_callbacks.any? { |name| name == method_name }
148
+ end
149
+
150
+ def qualify(names)
151
+ names.join('::')
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noraneko
4
+ class NMethod
5
+ attr_accessor :called_methods
6
+ attr_reader :name, :line
7
+ attr_writer :scope, :type
8
+
9
+ def initialize(nconst, name, line, scope, type)
10
+ @nconst = nconst
11
+ @name = name
12
+ @line = line
13
+ @called_methods = []
14
+ @scope = scope
15
+ @type = type
16
+ end
17
+
18
+ def self.instance_method(nconst, name, line, scope = :public)
19
+ new(nconst, name, line, scope, :instance)
20
+ end
21
+
22
+ def self.class_method(nconst, name, line)
23
+ new(nconst, name, line, :public, :class)
24
+ end
25
+
26
+ def loc
27
+ "#{@nconst.path}:#{@line}"
28
+ end
29
+
30
+ def in?(nconst)
31
+ nconst.qualified_name == @nconst.qualified_name
32
+ end
33
+
34
+ def called?(other_name)
35
+ @called_methods.include?(other_name)
36
+ end
37
+
38
+ def qualified_name
39
+ delimiter = class_method? ? '.' : '#'
40
+ "#{@nconst.qualified_name}#{delimiter}#{@name}"
41
+ end
42
+
43
+ def private!
44
+ @scope = :private
45
+ end
46
+
47
+ def in_public?
48
+ @scope == :public
49
+ end
50
+
51
+ def in_private?
52
+ !in_public?
53
+ end
54
+
55
+ def class_method!
56
+ @type = :class
57
+ end
58
+
59
+ def class_method?
60
+ @type == :class
61
+ end
62
+
63
+ def instance_method?
64
+ !class_method?
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noraneko
4
+ class NModule < ::Noraneko::NConst
5
+ end
6
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noraneko
4
+ module NodeUtility
5
+ def extract_consts(const_node, consts = [])
6
+ next_const_node, const_sym = const_node.children
7
+ consts.unshift(const_sym)
8
+ if next_const_node
9
+ extract_consts(next_const_node, consts)
10
+ else
11
+ consts
12
+ end
13
+ end
14
+
15
+ def extract_syms(nodes)
16
+ nodes.map { |n| n.children.last }
17
+ end
18
+
19
+ def convert_to_hash(node)
20
+ raise 'This is not hash expression' unless node.type == :hash
21
+ node.children.each_with_object({}) do |pair, hash|
22
+ key, value = pair.children
23
+ if convertable?(key) && convertable?(value)
24
+ hash[convert!(key)] = convert!(value)
25
+ end
26
+ end
27
+ end
28
+
29
+ def singleton_class?(node)
30
+ node&.children&.first&.type == :self
31
+ end
32
+
33
+ private
34
+
35
+ def convertable?(node)
36
+ %i[sym str].include?(node.type) ? true : false
37
+ end
38
+
39
+ def convert!(node)
40
+ node.children.last
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noraneko
4
+ class NView
5
+ attr_accessor :called_views
6
+ attr_reader :filepath
7
+
8
+ def initialize(filepath, type = :normal)
9
+ @filepath = filepath
10
+ @rel_path = filepath.split('/views/').drop(1).join('').split('.').first
11
+ @called_views = []
12
+ @type = type
13
+ end
14
+
15
+ def called?(other_name)
16
+ @called_views.include?(other_name)
17
+ end
18
+
19
+ def call_view(name)
20
+ @called_views << name
21
+ end
22
+
23
+ def loc
24
+ @filepath
25
+ end
26
+
27
+ def qualified_name
28
+ @rel_path
29
+ end
30
+
31
+ def name
32
+ @rel_path
33
+ end
34
+
35
+ def partial?
36
+ @type == :partial
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,276 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser/current'
4
+
5
+ # opt-in to most recent AST format:
6
+ Parser::Builders::Default.emit_lambda = true
7
+ Parser::Builders::Default.emit_procarg0 = true
8
+
9
+ module Noraneko
10
+ class Processor < ::Parser::AST::Processor
11
+ include Noraneko::NodeUtility
12
+
13
+ attr_writer :registry, :filepath, :context_stack
14
+
15
+ ParseError = Class.new(StandardError)
16
+
17
+ def self.init_with(registry:, filepath: nil)
18
+ new.tap do |instance|
19
+ instance.registry = registry
20
+ instance.filepath = filepath
21
+ instance.context_stack = []
22
+ end
23
+ end
24
+
25
+ def process(node)
26
+ return nil unless node
27
+ context_generated = false
28
+
29
+ begin
30
+ case node.type
31
+ when :class, :sclass
32
+ nclass = process_class(node)
33
+ context_generated = true
34
+ when :module
35
+ nmodule = process_module(node)
36
+ context_generated = true
37
+ when :def
38
+ process_def(node)
39
+ context_generated = true
40
+ when :defs
41
+ process_defs(node)
42
+ context_generated = true
43
+ when :send
44
+ process_send(node)
45
+ when :block_pass
46
+ process_block_pass(node)
47
+ end
48
+ rescue StandardError
49
+ line = node.loc.line
50
+ message = "Fail to parse. location: #{@filepath}:#{line}"
51
+ raise ParseError.new(message)
52
+ end
53
+
54
+ super
55
+
56
+ if node.type == :sclass
57
+ @registry.find(nclass.parent_name).merge_singleton(nclass)
58
+ @registry.delete(nclass)
59
+ end
60
+ if context_generated
61
+ @public_scope = true unless sent_in_method?
62
+ @context_stack.pop
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def process_class(node)
69
+ names = if singleton_class?(node)
70
+ %w[Self]
71
+ else
72
+ extract_consts(node.children.first)
73
+ end
74
+ qualified_name = current_context.child_qualified_name(names)
75
+ line = node.loc.line
76
+ nclass = NClass.new(qualified_name, @filepath, line)
77
+ @context_stack << nclass
78
+ @registry.put(nclass)
79
+ end
80
+
81
+ def process_module(node)
82
+ names = extract_consts(node.children.first)
83
+ qualified_name = current_context.child_qualified_name(names)
84
+ line = node.loc.line
85
+ nmodule = NModule.new(qualified_name, @filepath, line)
86
+ @context_stack << nmodule
87
+ @registry.put(nmodule)
88
+ end
89
+
90
+ def process_def(node)
91
+ method_name = node.children.first
92
+ line = node.loc.line
93
+ nmethod = current_context.add_method(method_name, line)
94
+ @context_stack << nmethod
95
+ end
96
+
97
+ def process_defs(node)
98
+ context = current_context
99
+ method_name = node.children[1]
100
+ line = node.loc.line
101
+ nmethod =
102
+ if sent_in_method?
103
+ receiver = node.children[0].children.first
104
+ context = NModule.new("#{context.qualified_name}##{receiver}", @filepath, 0)
105
+ @registry.put(context)
106
+ context.add_method(method_name, line)
107
+ else
108
+ context.add_cmethod(method_name, line)
109
+ end
110
+ @context_stack << nmethod
111
+ end
112
+
113
+ def process_send(node)
114
+ case node.children[1]
115
+ when :private
116
+ process_private(node)
117
+ when :include
118
+ process_include(node)
119
+ when :extend
120
+ process_extend(node)
121
+ when :alias_method
122
+ process_alias_method(node)
123
+ when :send
124
+ process_explicit_send(node)
125
+ else
126
+ if sent_in_method?
127
+ process_send_message(node)
128
+ else
129
+ process_send_nconst(node)
130
+ end
131
+ end
132
+ end
133
+
134
+ def process_alias_method(node)
135
+ aliased = node.children[2].children.last
136
+ line = node.loc.line
137
+ nmethod = current_context.add_method(aliased, line)
138
+ end
139
+
140
+ def process_external_import(node)
141
+ node.children.drop(2).each_with_object([]) do |target, consts|
142
+ if target.type == :const
143
+ const_name = extract_consts(target).join('::')
144
+ consts << const_name
145
+ end
146
+ end
147
+ end
148
+
149
+ def process_include(node)
150
+ current_context.included_module_names += process_external_import(node)
151
+ end
152
+
153
+ def process_extend(node)
154
+ current_context.extended_module_names += process_external_import(node)
155
+ end
156
+
157
+ def process_private(node)
158
+ if node.children.size == 2
159
+ current_context.private!
160
+ else
161
+ extract_syms(node.children.drop(2)).each do |method_name|
162
+ current_context.make_method_private(method_name)
163
+ end
164
+ end
165
+ end
166
+
167
+ def process_send_nconst(node)
168
+ case node.children[1]
169
+ when :module_function
170
+ syms = extract_syms(node.children[2..-1])
171
+ if syms.empty?
172
+ current_context.method_default_as_class!
173
+ else
174
+ syms.each { |sym| current_context.find_method(sym).class_method! }
175
+ end
176
+ when :layout
177
+ process_layout(node)
178
+ else
179
+ current_context.register_csend(node.children[1])
180
+ process_callback_register(node)
181
+ end
182
+ end
183
+
184
+ def process_layout(node)
185
+ param = node.children[2]
186
+ if [:str, :sym].include?(param.type)
187
+ layout = param.children.last.to_s
188
+ current_context.called_view('layouts/' + layout)
189
+ end
190
+ end
191
+
192
+ def process_callback_register(node)
193
+ return if node.children.size < 3 || !node.children.first.nil?
194
+ name = node.children[1]
195
+ syms = node.children.drop(2).select { |n| n.type == :sym }
196
+ return if syms.empty?
197
+ current_context.registered_callbacks += extract_syms(syms)
198
+ end
199
+
200
+ def process_explicit_send(node)
201
+ target_node= node.children[2]
202
+ return unless [:str, :sym].include?(target_node.type)
203
+ called_method_name = target_node.children.last.to_sym
204
+ current_method_name = current_context.name
205
+ parent_context.register_send(current_method_name, called_method_name)
206
+ end
207
+
208
+ def process_send_message(node)
209
+ if parent_context.controller? && node.children[1] == :render
210
+ process_render(node)
211
+ else
212
+ current_method_name = current_context.name
213
+ called_method_name = node.children[1]
214
+ parent_context.register_send(current_method_name, called_method_name)
215
+ end
216
+ end
217
+
218
+ def process_block_pass(node)
219
+ sym = node.children.first
220
+ current_method_name = current_context.name
221
+ called_method_name = sym.children.last
222
+ parent_context.register_send(current_method_name, called_method_name)
223
+ end
224
+
225
+ def process_render(node)
226
+ params = node.children.drop(2).first
227
+ return unless params
228
+ view_name = extract_view_name(params)
229
+ return unless view_name
230
+ parent_context.called_view(view_name)
231
+ end
232
+
233
+ def rel_path_from_controller(controller)
234
+ controller.path
235
+ .split('/controllers/').drop(1).join('')
236
+ .split('_controller.rb').first + '/'
237
+ end
238
+
239
+ def extract_view_name(param)
240
+ value =
241
+ if param.type == :hash
242
+ hash = convert_to_hash(param)
243
+ hash[:action] || hash[:template]
244
+ else
245
+ param.children.last
246
+ end
247
+ # Inline render
248
+ return nil unless value
249
+
250
+ view_path = value.to_s.split('.').first
251
+ if view_path.split('/').size == 1
252
+ parent_context.rel_path_from_controller + view_path
253
+ else
254
+ view_path
255
+ end
256
+ end
257
+
258
+ def parent_context
259
+ @context_stack[-2] || global_const
260
+ end
261
+
262
+ def current_context
263
+ @context_stack.last || global_const
264
+ end
265
+
266
+ def sent_in_method?
267
+ current_context.is_a? Noraneko::NMethod
268
+ end
269
+
270
+ def global_const
271
+ return @_global_nconst if @_global_nconst
272
+ @_global_nconst = NModule.new('', @filepath, 0)
273
+ @registry.put(@_global_nconst)
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noraneko
4
+ class Project
5
+ RESERVED_METHODS = %i[initialize self].freeze
6
+
7
+ def initialize(registry, view_registry)
8
+ @registry = registry
9
+ @nconsts = registry.to_a
10
+ @view_registry = view_registry
11
+ @views = view_registry.to_a
12
+ end
13
+
14
+ def unused_methods
15
+ (unused_private_methods + unused_public_methods).reject do |method|
16
+ RESERVED_METHODS.include?(method.name)
17
+ end
18
+ end
19
+
20
+ def unused_modules
21
+ @nconsts.each_with_object([]) do |nconst, candidates|
22
+ nconst.all_used_modules.each do |m_name|
23
+ cmodule = @registry.find(m_name)
24
+ next unless cmodule
25
+ if cmodule.all_methods.all? { |method| unused_public_method?(method) }
26
+ candidates << cmodule
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def unused_views
33
+ controllers = @nconsts.select { |n| n.controller? }
34
+ @views.reject { |v| v.name == 'layouts/application' }.select do |view|
35
+ controllers.none? { |con| con.used_view?(view.name) } &&
36
+ @views.none? { |v| v.called?(view.name) }
37
+ end
38
+ end
39
+
40
+ def all_unuseds
41
+ unused_methods + unused_modules + unused_views
42
+ end
43
+
44
+ private
45
+
46
+ def unused_private_methods
47
+ methods = @nconsts.map(&:all_private_methods).flatten
48
+ methods.each_with_object([]) do |method, candidates|
49
+ # FIX: Inherit is not supported, so it handled as public method
50
+ candidates << method if unused_public_method?(method)
51
+ end
52
+ end
53
+
54
+ def unused_public_methods
55
+ methods = @nconsts.map(&:all_public_methods).flatten
56
+ methods.each_with_object([]) do |method, candidates|
57
+ candidates << method if unused_public_method?(method)
58
+ end
59
+ end
60
+
61
+ def unused_public_method?(method)
62
+ @nconsts.none? { |nconst| nconst.used?(method) }
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noraneko
4
+ class Registry
5
+ def initialize
6
+ @namespace = {}
7
+ end
8
+
9
+ def find(name)
10
+ @namespace[name]
11
+ end
12
+
13
+ def put(nconst)
14
+ @namespace[nconst.qualified_name] = nconst
15
+ end
16
+
17
+ def delete(nconst)
18
+ @namespace.delete(nconst.qualified_name)
19
+ end
20
+
21
+ def to_a
22
+ @namespace.values
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noraneko
4
+ class Runner
5
+ def run(paths)
6
+ normalized = normalize_paths(paths)
7
+ registry = analyze_ruby_files(normalized)
8
+ view_registry = analyze_view_files(normalized)
9
+ Project.new(registry, view_registry).all_unuseds
10
+ end
11
+
12
+ private
13
+
14
+ def normalize_paths(paths)
15
+ paths.map do |path|
16
+ if path.end_with?('/')
17
+ path[0..-2]
18
+ else
19
+ path
20
+ end
21
+ end
22
+ end
23
+
24
+ def find_ruby_files(paths)
25
+ paths.map { |path| find_ruby_files_in_path(path) }.flatten
26
+ end
27
+
28
+ def find_ruby_files_in_path(path)
29
+ Dir["#{path}/**/*.rb"].reject do |file|
30
+ file.match?(/\/(spec|test|db)\//)
31
+ end
32
+ end
33
+
34
+ def find_view_files(paths)
35
+ paths.map { |path| Dir["#{path}/**/app/views/**/*.*"] }.flatten
36
+ end
37
+
38
+ def analyze_ruby_files(paths)
39
+ target_files = find_ruby_files(paths)
40
+ Noraneko::Registry.new.tap do |registry|
41
+ target_files.each do |file|
42
+ processor =
43
+ Noraneko::Processor.init_with(registry: registry, filepath: file)
44
+ source = File.read(file)
45
+ ast = Parser::CurrentRuby.parse(source)
46
+ processor.process(ast)
47
+ end
48
+ end
49
+ end
50
+
51
+ def analyze_view_files(paths)
52
+ target_files = find_view_files(paths)
53
+ Noraneko::Registry.new.tap do |registry|
54
+ target_files.each do |file|
55
+ processor =
56
+ Noraneko::ViewProcessor.new(registry: registry, filepath: file)
57
+ source = File.read(file)
58
+ processor.process(source)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noraneko
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,47 @@
1
+ module Noraneko
2
+ class ViewProcessor
3
+ def initialize(registry:, filepath:)
4
+ @registry = registry
5
+ @filepath = filepath
6
+ @nview = Noraneko::NView.new(@filepath, :partial)
7
+ registry.put(@nview)
8
+ end
9
+
10
+ def process(text)
11
+ text.each_line do |line|
12
+ process_line(line)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def has_render?(line)
19
+ line.match?(/render[\s(]/)
20
+ end
21
+
22
+ def process_line(line)
23
+ return unless has_render?(line)
24
+ matched = line.match(/\srender[\s(]+(['"])(.+)(\1)/)
25
+ if !matched
26
+ matched = line.match(/[\s(]partial.+(['"])(.+)(\1)/)
27
+ end
28
+ return unless matched
29
+
30
+ name =
31
+ if matched[2].split('/').size == 1
32
+ rel_path_from_view + '/_' + matched[2]
33
+ else
34
+ *prefix, name = matched[2].split('/')
35
+ prefix.join('/') + '/_' + name
36
+ end
37
+
38
+ @nview.call_view(name)
39
+ end
40
+
41
+ def rel_path_from_view
42
+ @nview.filepath
43
+ .split('/views/').drop(1).join('')
44
+ .split('/')[0..-2].join('')
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ lib = File.expand_path('../lib', __FILE__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require 'noraneko/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'noraneko'
10
+ spec.version = Noraneko::VERSION
11
+ spec.authors = ['Shia']
12
+ spec.email = ['rise.shia@gmail.com']
13
+
14
+ spec.summary = 'Find candidate which unused methods, views from rails app'
15
+ spec.description = 'Find candidate unused methods, views from rails app with static parse'
16
+ spec.homepage = 'https://github.com/riseshia/noraneko'
17
+ spec.license = 'MIT'
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
20
+ f.match(%r{^(test|spec|features)/})
21
+ end
22
+ spec.bindir = 'exe'
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ['lib']
25
+
26
+ spec.add_development_dependency 'bundler', '~> 1.15'
27
+ spec.add_development_dependency 'rake', '~> 10.0'
28
+ spec.add_development_dependency 'rspec', '~> 3.0'
29
+ spec.add_dependency 'parser', '~> 2.4'
30
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: noraneko
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shia
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-08-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.15'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.15'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: parser
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.4'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.4'
69
+ description: Find candidate unused methods, views from rails app with static parse
70
+ email:
71
+ - rise.shia@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".rspec"
78
+ - ".ruby-version"
79
+ - ".travis.yml"
80
+ - CODE_OF_CONDUCT.md
81
+ - Gemfile
82
+ - LICENSE.txt
83
+ - README.md
84
+ - Rakefile
85
+ - bin/console
86
+ - bin/noraneko
87
+ - bin/setup
88
+ - lib/noraneko.rb
89
+ - lib/noraneko/cli.rb
90
+ - lib/noraneko/nclass.rb
91
+ - lib/noraneko/nconst.rb
92
+ - lib/noraneko/nmethod.rb
93
+ - lib/noraneko/nmodule.rb
94
+ - lib/noraneko/node_utility.rb
95
+ - lib/noraneko/nview.rb
96
+ - lib/noraneko/processor.rb
97
+ - lib/noraneko/project.rb
98
+ - lib/noraneko/registry.rb
99
+ - lib/noraneko/runner.rb
100
+ - lib/noraneko/version.rb
101
+ - lib/noraneko/view_processor.rb
102
+ - noraneko.gemspec
103
+ homepage: https://github.com/riseshia/noraneko
104
+ licenses:
105
+ - MIT
106
+ metadata: {}
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 2.6.10
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: Find candidate which unused methods, views from rails app
127
+ test_files: []
128
+ has_rdoc: