avo-linter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e192558b6e6c040bed45f0a10bbd3c822980e412a4a2ea8a4ff524ff1314d480
4
+ data.tar.gz: cde77967db5c23805bfdf329357b55f3466998858a362f909560c26b3122cb78
5
+ SHA512:
6
+ metadata.gz: 3291101920e425058c5459a66c689bb5e817dddf2adf326c4e96ea5475af7ea0af2999ff60a3003012290297e75cc97dfc882443262775a777247bd10274b0bd
7
+ data.tar.gz: b7e07e18355ce3c711e7338c1c4d5954a6c356c8dbe9f07821742df740b1373113d580a0c64429de45c7336d24501f02e4438518d91fead564fdb91d961368bd
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ ruby "~> 3.3.0"
6
+
7
+ gem "awesome_print"
8
+ gem "dry-initializer"
9
+ gem "activesupport", "> 7.0.4", "< 7.1.0"
10
+ gem "dry-cli"
11
+ gem "paint"
12
+ gem "tty-command"
13
+ gem "zeitwerk"
14
+ gem "prism"
15
+ gem "parser"
16
+ gem "parser-prism"
17
+ gem "ruby-lsp"
18
+ gem "rspec"
19
+ gem "rspec-expectations"
data/Gemfile.lock ADDED
@@ -0,0 +1,78 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ activesupport (7.0.8)
5
+ concurrent-ruby (~> 1.0, >= 1.0.2)
6
+ i18n (>= 1.6, < 2)
7
+ minitest (>= 5.1)
8
+ tzinfo (~> 2.0)
9
+ ast (2.4.2)
10
+ awesome_print (1.9.2)
11
+ concurrent-ruby (1.2.2)
12
+ diff-lcs (1.5.0)
13
+ dry-cli (1.0.0)
14
+ dry-initializer (3.1.1)
15
+ i18n (1.14.1)
16
+ concurrent-ruby (~> 1.0)
17
+ language_server-protocol (3.17.0.3)
18
+ minitest (5.20.0)
19
+ paint (2.3.0)
20
+ parser (3.3.0.2)
21
+ ast (~> 2.4.1)
22
+ racc
23
+ parser-prism (0.1.0)
24
+ parser
25
+ prism
26
+ pastel (0.8.0)
27
+ tty-color (~> 0.5)
28
+ prism (0.19.0)
29
+ racc (1.7.3)
30
+ rspec (3.12.0)
31
+ rspec-core (~> 3.12.0)
32
+ rspec-expectations (~> 3.12.0)
33
+ rspec-mocks (~> 3.12.0)
34
+ rspec-core (3.12.2)
35
+ rspec-support (~> 3.12.0)
36
+ rspec-expectations (3.12.3)
37
+ diff-lcs (>= 1.2.0, < 2.0)
38
+ rspec-support (~> 3.12.0)
39
+ rspec-mocks (3.12.6)
40
+ diff-lcs (>= 1.2.0, < 2.0)
41
+ rspec-support (~> 3.12.0)
42
+ rspec-support (3.12.1)
43
+ ruby-lsp (0.13.2)
44
+ language_server-protocol (~> 3.17.0)
45
+ prism (>= 0.19.0, < 0.20)
46
+ sorbet-runtime (>= 0.5.5685)
47
+ sorbet-runtime (0.5.11180)
48
+ tty-color (0.6.0)
49
+ tty-command (0.10.1)
50
+ pastel (~> 0.8)
51
+ tzinfo (2.0.6)
52
+ concurrent-ruby (~> 1.0)
53
+ zeitwerk (2.6.12)
54
+
55
+ PLATFORMS
56
+ arm64-darwin-23
57
+ ruby
58
+
59
+ DEPENDENCIES
60
+ activesupport (> 7.0.4, < 7.1.0)
61
+ awesome_print
62
+ dry-cli
63
+ dry-initializer
64
+ paint
65
+ parser
66
+ parser-prism
67
+ prism
68
+ rspec
69
+ rspec-expectations
70
+ ruby-lsp
71
+ tty-command
72
+ zeitwerk
73
+
74
+ RUBY VERSION
75
+ ruby 3.3.0p0
76
+
77
+ BUNDLED WITH
78
+ 2.5.3
data/LICENSE.md ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2023 Adrian Marin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,26 @@
1
+ require_relative "lib/avo_linter/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "avo-linter"
5
+ spec.version = AvoLinter::VERSION
6
+ spec.summary = "Linter for Avo CMS for Ruby on Rails"
7
+ spec.description = "The linter scans your Avo app and exposes errors"
8
+ spec.authors = ["Adrian Marin"]
9
+ spec.email = "adrian@adrianthedev.com"
10
+ spec.files = Dir["{bin,lib}/**/*", "LICENSE.MD", "readme.md", "avo-linter.gemspec", "Gemfile", "Gemfile.lock", "linter.png"]
11
+ spec.homepage = "https://avohq.io"
12
+ spec.license = "MIT"
13
+
14
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
15
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
16
+ if spec.respond_to?(:metadata)
17
+ spec.metadata["bug_tracker_uri"] = "https://github.com/avo-hq/avo-linter/issues"
18
+ spec.metadata["changelog_uri"] = "https://github.com/avo-hq/avo-linter/releases"
19
+ spec.metadata["documentation_uri"] = "https://github.com/avo-hq/avo-linter"
20
+ spec.metadata["homepage_uri"] = "https://avohq.io"
21
+ spec.metadata["source_code_uri"] = "https://github.com/avo-hq/avo-linter"
22
+ else
23
+ raise "RubyGems 2.0 or newer is required to protect against " \
24
+ "public gem pushes."
25
+ end
26
+ end
data/bin/avo ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $:.unshift(File.expand_path("../", __dir__))
5
+
6
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
7
+
8
+ require_relative "../lib/avo_linter"
9
+ require_relative "../lib/avo_linter/cli"
data/bin/rspec ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ load Gem.bin_path('rspec-core', 'rspec')
@@ -0,0 +1,60 @@
1
+ module AvoLinter
2
+ VERSION = "0.0.1"
3
+
4
+ module CLI
5
+ module Commands
6
+ extend Dry::CLI::Registry
7
+
8
+ class BaseCommand < Dry::CLI::Command
9
+ def run(command)
10
+ result = cmd.run(command)
11
+ halt if result.failed?
12
+ result
13
+ rescue TTY::Command::ExitError
14
+ halt
15
+ end
16
+
17
+ def halt(message: nil)
18
+ message ||= "#{self.class} failed."
19
+ yell message
20
+ exit
21
+ end
22
+ end
23
+
24
+ class Version < BaseCommand
25
+ desc "Print version"
26
+
27
+ def call(*)
28
+ puts VERSION
29
+ end
30
+ end
31
+
32
+ class Lint < BaseCommand
33
+ desc "Lint gem"
34
+
35
+ option :path, aliases: ["-p"], required: false, desc: "Path of your Rails app"
36
+
37
+ def call(**)
38
+ scan = ::AvoLinter::Scanner.scan
39
+
40
+ # ap scan
41
+
42
+ say "Scan finished!\n"
43
+
44
+ if scan.errors?
45
+ yell "We found a couple of errors."
46
+ ap scan.error_messages
47
+ else
48
+ say "Nothing bad found. Good job!"
49
+ end
50
+ end
51
+ end
52
+
53
+ register "version", Version, aliases: ["v", "-v", "--version"]
54
+
55
+ register "lint", Lint
56
+ end
57
+ end
58
+ end
59
+
60
+ Dry::CLI.new(AvoLinter::CLI::Commands).call
@@ -0,0 +1,31 @@
1
+ class AvoLinter::Rules::Base
2
+ attr_reader :contents
3
+ attr_reader :errors
4
+
5
+ def initialize(contents:, **)
6
+ @contents = contents
7
+ @errors = []
8
+ end
9
+
10
+ def apply_rule
11
+ apply
12
+
13
+ self
14
+ end
15
+
16
+ def message = self.class::MESSAGE
17
+
18
+ private
19
+
20
+ def get_class_body
21
+ parsed_contents.value.statements.body.first.body.body
22
+ end
23
+
24
+ def parsed_contents
25
+ Prism.parse contents
26
+ end
27
+
28
+ def error_out(message)
29
+ @errors << message
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+ class AvoLinter::Rules::FieldsAsClassMethods < AvoLinter::Rules::Base
2
+ MESSAGE = "You should not use the `field` method as a class method. Please add it in the `def fields` method or use composition in other methods."
3
+
4
+ def apply
5
+ get_class_body.each do |node|
6
+ if node.instance_of?(Prism::CallNode) && node.name == :field
7
+ error_out(
8
+ message:,
9
+ node:,
10
+ )
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,42 @@
1
+ class AvoLinter::Scanner
2
+ AVO_PATHS = {
3
+ actions: ["app", "avo", "actions", "*.rb"],
4
+ config: ["config", "*.rb"],
5
+ cards: ["app", "avo", "cards", "*.rb"],
6
+ dashboards: ["app", "avo", "dashboards", "*.rb"],
7
+ fields: ["app", "avo", "fields", "*.rb"],
8
+ filters: ["app", "avo", "filters", "*.rb"],
9
+ models: ["app", "models", "*.rb"],
10
+ resource_controllers: ["app", "controllers", "avo", "*.rb"],
11
+ resource_tools: ["app", "avo", "resource_tools", "*.rb"],
12
+ resources: ["app", "avo", "resources", "*.rb"],
13
+ scopes: ["app", "avo", "scopes", "*.rb"],
14
+ views: ["app", "views", "avo", "*.rb"]
15
+ }
16
+
17
+ def self.scan(path: Dir.pwd)
18
+ scan = new
19
+ AvoLinter::Scanners::ResourceScanner.new(path:, scan:).scan!
20
+ scan
21
+ end
22
+
23
+ attr_reader :results
24
+
25
+ def initialize
26
+ @results = []
27
+ end
28
+
29
+ def error_messages
30
+ errors.flatten.map do |error|
31
+ error[:message]
32
+ end
33
+ end
34
+
35
+ def errors
36
+ results.map(&:errors).flatten
37
+ end
38
+
39
+ def errors?
40
+ errors.present?
41
+ end
42
+ end
@@ -0,0 +1,102 @@
1
+ require "ruby-lsp"
2
+ require "prism"
3
+ require "parser/prism"
4
+
5
+ class AvoLinter::Scanners::Base
6
+ attr_reader :path
7
+ attr_reader :scan
8
+
9
+ def initialize(path:, scan:)
10
+ @path = path
11
+ @scan = scan
12
+ end
13
+
14
+ # def scan
15
+ # puts ["scanning->"].inspect
16
+
17
+ # # files.each do |file|
18
+ # # puts file
19
+ # # end
20
+ # # ap files
21
+
22
+ # file_path = "/Users/adrian/work/avocado/avohq.io-v3/app/avo/resources/account.rb"
23
+ # file_path = "/Users/adrian/work/avocado/gems/avo-linter/account.rb"
24
+ # contents = File.read(file_path)
25
+ # # puts ["contents->", contents].inspect
26
+ # # @index = RubyIndexer::Index.new
27
+ # # puts ["@index->", @index].inspect
28
+ # # return
29
+ # if false
30
+ # parsed = Parser::Prism.parse_file file_path
31
+ # puts ["parsed->", parsed].inspect
32
+ # else
33
+ # # parsed = Prism.parse contents
34
+ # # fields_tree = parsed.value.statements.body.first.body.body.find do |item|
35
+ # # puts ["item->", item.class].inspect
36
+ # # item.instance_of?(Prism::DefNode) && item.name == :fields
37
+ # # # matches?(item, Prism::DefNode)
38
+ # # end
39
+
40
+ # # scan_resource_files
41
+ # AvoLinter::Scanners::ResourceScanner.new.scan
42
+
43
+ # # fields =
44
+ # # puts ["fields_tree!!->", get_fields_in_fields_method(contents)].inspect
45
+ # # puts ["apply->", AvoLinter::Rules::FieldsAsClassMethods.new.apply].inspect
46
+ # # puts ["parsed->", parsed.value.statements.body.first.body.body].inspect
47
+ # end
48
+ # end
49
+
50
+ private
51
+
52
+ def files
53
+ # # Read ignore patterns from the .gitignore file in the directory
54
+ gitignore_path = File.join(path, ".gitignore")
55
+ ignore_patterns = get_ignore_patterns_from_gitignore(gitignore_path)
56
+ puts ["ignore_patterns->", ignore_patterns].inspect
57
+
58
+ get_all_files_in_directory_and_subdirectories(path, ignore_patterns)
59
+ end
60
+
61
+ def paths_to_scan
62
+ [
63
+ ["app", "avo", "**", "*.rb"],
64
+ ["app", "controllers", "avo", "**", "*.rb"],
65
+ ["app", "models", "**", "*.rb"],
66
+ ["app", "views", "avo", "**", "*.erb"],
67
+ ["config", "**", "*.rb"]
68
+ ]
69
+ end
70
+
71
+ def get_ignore_patterns_from_gitignore(gitignore_path)
72
+ ignore_patterns = []
73
+
74
+ if File.exist?(gitignore_path)
75
+ File.readlines(gitignore_path).each do |line|
76
+ # Remove leading and trailing whitespace
77
+ pattern = line.strip
78
+ next if pattern.empty? || pattern.start_with?("#") # Skip comments and empty lines
79
+ ignore_patterns << pattern
80
+ end
81
+ end
82
+
83
+ ignore_patterns
84
+ end
85
+
86
+ def get_all_files_in_directory_and_subdirectories(directory, ignore_patterns)
87
+ all_files = []
88
+
89
+ # ap paths_to_scan
90
+ # return []
91
+ paths_to_scan.each do |path_to_scan|
92
+ Dir.glob(File.join(*path_to_scan)).each do |file|
93
+ next if File.directory?(file)
94
+ # next if ignore_patterns.any? { |pattern| File.fnmatch?(pattern, file) }
95
+
96
+ all_files << File.expand_path(file)
97
+ end
98
+ end
99
+
100
+ all_files.flatten
101
+ end
102
+ end
@@ -0,0 +1,39 @@
1
+ class AvoLinter::Scanners::ResourceScanner < AvoLinter::Scanners::Base
2
+ # def initialize
3
+
4
+ # end
5
+
6
+ def scan!
7
+ # puts ["resource_files->", resource_files].inspect
8
+
9
+ # errors = []
10
+ resource_files.map do |path, contents|
11
+ results = AvoLinter::Rules::FieldsAsClassMethods.new(contents:).apply_rule
12
+ scan.results << results
13
+ # scan.errors << results.errors
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ # def scan_resource_files
20
+ # resource_files
21
+ # end
22
+
23
+ def resource_files
24
+ resource_file_paths.map do |file_path|
25
+ [file_path, File.read(file_path)]
26
+ end.to_h
27
+ end
28
+
29
+ def resource_file_paths
30
+ all_files = []
31
+
32
+ Dir.glob(File.join(path, *AvoLinter::Scanner::AVO_PATHS[:resources])).each do |file|
33
+ next if File.directory?(file)
34
+ all_files << file
35
+ end
36
+
37
+ all_files
38
+ end
39
+ end
@@ -0,0 +1,3 @@
1
+ module AvoLinter
2
+ VERSION = "0.0.1"
3
+ end
data/lib/avo_linter.rb ADDED
@@ -0,0 +1,22 @@
1
+ require "bundler/setup"
2
+ require "dry/cli"
3
+ require "active_support/core_ext/class/attribute"
4
+ require "yaml"
5
+ require "tty-command"
6
+ require "fileutils"
7
+ require "active_support/core_ext/string"
8
+ require "pathname"
9
+ require "rubygems"
10
+ require "zeitwerk"
11
+ require "awesome_print"
12
+ require_relative "support"
13
+
14
+ loader = Zeitwerk::Loader.for_gem
15
+ loader.ignore("#{__dir__}/support.rb")
16
+ loader.inflector.inflect(
17
+ "cli" => "CLI"
18
+ )
19
+ loader.setup
20
+
21
+ module AvoLinter
22
+ end
data/lib/support.rb ADDED
@@ -0,0 +1,52 @@
1
+ def say(text)
2
+ puts "=> #{yellow(text)}"
3
+ end
4
+
5
+ def yell(text)
6
+ puts "=> #{red(text)}"
7
+ end
8
+
9
+ def colorize(text, color_code)
10
+ "#{color_code}#{text}\e[0m"
11
+ end
12
+
13
+ def yellow(text)
14
+ colorize(text, "\e[33m")
15
+ end
16
+
17
+ def red(text)
18
+ colorize(text, "\e[31m")
19
+ end
20
+
21
+ def green(text)
22
+ colorize(text, "\e[32m")
23
+ end
24
+
25
+ def gemspec_path
26
+ Dir["#{Dir.pwd}/*.gemspec"].first
27
+ end
28
+
29
+ def gemspec
30
+ Gem::Specification.load(gemspec_path)
31
+ end
32
+
33
+ def version
34
+ @version ||= gemspec.version.to_s
35
+ end
36
+
37
+ def gemspec_name
38
+ gemspec.name
39
+ end
40
+
41
+ def cmd
42
+ TTY::Command.new uuid: false
43
+ end
44
+
45
+ def change_in_file(file, regex, text_to_put_in_place)
46
+ text = File.read file
47
+ File.open(file, "w+") { |f| f << text.gsub(/#{regex}/, text_to_put_in_place) }
48
+ end
49
+
50
+ def bundler_token
51
+ @token ||= `bundle config get https://packager.dev/avo-hq`.match(/^.*: "(.*)"$/).captures.first
52
+ end
data/linter.png ADDED
Binary file
data/readme.md ADDED
@@ -0,0 +1,40 @@
1
+ # Avo Linter
2
+
3
+ > [!WARNING]
4
+ > This is highly experimental.
5
+
6
+ This CLI is used by Avo, the Avo LSP (coming soon), and other products to lint the Avo configuration files.
7
+
8
+ It will show you errors that you might have missed in the Avo files along with improvements that you could make.
9
+
10
+ ![](./linter.png)
11
+
12
+ ## Running it
13
+
14
+ > [!WARNING]
15
+ > Temporary.
16
+
17
+ Git clone it
18
+
19
+ ## Overview
20
+
21
+ The linter uses different techniques to figure out if Avo configuration files are invalid or could be improved
22
+
23
+ #### Using Prism
24
+
25
+ The linter is using [prism](https://github.com/ruby/prism) to parse the files and create an AST for each one.
26
+ We the linter is run it will scan the files and return errors it found in the files.
27
+
28
+ It's doing that by scanning the AST for common patterns using user-defined rules similar to how rubocop is working.
29
+
30
+ ### Installation
31
+
32
+ Run `bundle install` to install the dependencies.
33
+
34
+ ```bash
35
+ bundle
36
+ ```
37
+
38
+ ### Testing
39
+
40
+ Rspec is used for tests. Run `bin/rspec` to run all tests.
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: avo-linter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Adrian Marin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-01-11 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: The linter scans your Avo app and exposes errors
14
+ email: adrian@adrianthedev.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - Gemfile
20
+ - Gemfile.lock
21
+ - LICENSE.md
22
+ - avo-linter.gemspec
23
+ - bin/avo
24
+ - bin/rspec
25
+ - lib/avo_linter.rb
26
+ - lib/avo_linter/cli.rb
27
+ - lib/avo_linter/rules/base.rb
28
+ - lib/avo_linter/rules/fields_as_class_methods.rb
29
+ - lib/avo_linter/scanner.rb
30
+ - lib/avo_linter/scanners/base.rb
31
+ - lib/avo_linter/scanners/resource_scanner.rb
32
+ - lib/avo_linter/version.rb
33
+ - lib/support.rb
34
+ - linter.png
35
+ - readme.md
36
+ homepage: https://avohq.io
37
+ licenses:
38
+ - MIT
39
+ metadata:
40
+ bug_tracker_uri: https://github.com/avo-hq/avo-linter/issues
41
+ changelog_uri: https://github.com/avo-hq/avo-linter/releases
42
+ documentation_uri: https://github.com/avo-hq/avo-linter
43
+ homepage_uri: https://avohq.io
44
+ source_code_uri: https://github.com/avo-hq/avo-linter
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.5.3
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: Linter for Avo CMS for Ruby on Rails
64
+ test_files: []