typeguard 1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9e491b4197296ce89855941107dd841b9210d0a225b38d2981ebb69fc10f315d
4
+ data.tar.gz: 63abb9ec3829ca2e248ad24ac064f0be3db448353c7f89fade79a971d1064d15
5
+ SHA512:
6
+ metadata.gz: bfa33482c19eac191c3a8135df68b23d2258fbd9ca82c568234991a3168fa41213f2d13a50241beb7dab0a6d1866e41d2a78ba629027f80886c5114e05c9c98b
7
+ data.tar.gz: 11f22232e0a28552f52af7bd84a5c27c687496312375f741ac5b3c1b9dfc20bcfe26159519374e445f58503520752d196813813529052b991a81324df9a65e05
data/.editorconfig ADDED
@@ -0,0 +1,8 @@
1
+ root = true
2
+
3
+ [*]
4
+ end_of_line = lf
5
+ insert_final_newline = true
6
+ charset = utf-8
7
+ indent_style = space
8
+ indent_size = 2
data/.rubocop.yml ADDED
@@ -0,0 +1,25 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ AllCops:
4
+ TargetRubyVersion: 3.1
5
+
6
+ Metrics/AbcSize:
7
+ Enabled: false
8
+
9
+ Metrics/CyclomaticComplexity:
10
+ Enabled: false
11
+
12
+ Metrics/PerceivedComplexity:
13
+ Enabled: false
14
+
15
+ Metrics/BlockLength:
16
+ Enabled: false
17
+
18
+ Metrics/ClassLength:
19
+ Enabled: false
20
+
21
+ Metrics/MethodLength:
22
+ Enabled: false
23
+
24
+ Metrics/ModuleLength:
25
+ Enabled: false
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,89 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config --exclude-limit 30`
3
+ # on 2025-03-27 09:25:41 UTC using RuboCop version 1.71.1.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 1
10
+ # This cop supports unsafe autocorrection (--autocorrect-all).
11
+ Lint/BooleanSymbol:
12
+ Exclude:
13
+ - 'test/typeguard/type_model/test_parser.rb'
14
+
15
+ # Offense count: 8
16
+ # Configuration parameters: AllowedParentClasses.
17
+ Lint/MissingSuper:
18
+ Exclude:
19
+ - 'lib/typeguard/types.rb'
20
+
21
+ # Offense count: 1
22
+ # This cop supports safe autocorrection (--autocorrect).
23
+ # Configuration parameters: AutoCorrect, AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions.
24
+ # NotImplementedExceptions: NotImplementedError
25
+ Lint/UnusedMethodArgument:
26
+ Exclude:
27
+ - 'lib/typeguard/types.rb'
28
+
29
+ # Offense count: 8
30
+ # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
31
+ Metrics/ParameterLists:
32
+ Max: 9
33
+
34
+ # Offense count: 5
35
+ # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
36
+ # AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to
37
+ Naming/MethodParameterName:
38
+ Exclude:
39
+ - 'lib/typeguard/type_model/parser.rb'
40
+ - 'lib/typeguard/example.rb'
41
+
42
+ # Offense count: 7
43
+ # This cop supports unsafe autocorrection (--autocorrect-all).
44
+ # Configuration parameters: EnforcedStyle.
45
+ # SupportedStyles: nested, compact
46
+ Style/ClassAndModuleChildren:
47
+ Exclude:
48
+ - 'lib/typeguard/validator.rb'
49
+ - 'lib/typeguard/main.rb'
50
+
51
+ # Offense count: 34
52
+ # Configuration parameters: AllowedConstants.
53
+ Style/Documentation:
54
+ Exclude:
55
+ - 'spec/**/*'
56
+ - 'test/**/*'
57
+ - 'lib/typeguard/type_model/builder.rb'
58
+ - 'lib/typeguard/type_model/definitions.rb'
59
+ - 'lib/typeguard/type_model/parser.rb'
60
+ - 'lib/typeguard/validator.rb'
61
+ - 'lib/typeguard/configuration.rb'
62
+ - 'lib/typeguard/example.rb'
63
+ - 'lib/typeguard/main.rb'
64
+ - 'lib/typeguard/metrics.rb'
65
+ - 'lib/typeguard/resolver.rb'
66
+ - 'lib/typeguard/types.rb'
67
+ - 'lib/typeguard/validator.rb'
68
+ - 'lib/typeguard/wrapper.rb'
69
+
70
+ # Offense count: 2
71
+ # This cop supports safe autocorrection (--autocorrect).
72
+ # Configuration parameters: AutoCorrect, EnforcedStyle.
73
+ # SupportedStyles: compact, expanded
74
+ Style/EmptyMethod:
75
+ Exclude:
76
+ - 'test/typeguard/test_validator.rb'
77
+
78
+ # Offense count: 6
79
+ # This cop supports safe autocorrection (--autocorrect).
80
+ Style/IfUnlessModifier:
81
+ Exclude:
82
+ - 'lib/typeguard/validator.rb'
83
+
84
+ # Offense count: 2
85
+ # This cop supports safe autocorrection (--autocorrect).
86
+ # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
87
+ # URISchemes: http, https
88
+ Layout/LineLength:
89
+ Max: 228
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in typeguard.gemspec
6
+ gemspec
7
+
8
+ group :developement, :test do
9
+ gem 'rake', '~> 13.0'
10
+
11
+ gem 'minitest', '~> 5.0'
12
+
13
+ gem 'rubocop'
14
+
15
+ gem 'bundler-audit'
16
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tesorion
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # Typeguard
2
+
3
+ Runtime type checking for Ruby type signatures. Currently supports YARD and a subset of RBS.
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ $ bundle add typeguard
10
+
11
+ If bundler is not being used to manage dependencies, install the gem by executing:
12
+
13
+ $ gem install typeguard
14
+
15
+ ## Usage
16
+ Call `configure` and `process!` at the end of the original code to add type checking to it.
17
+
18
+ ```ruby
19
+ require 'typeguard'
20
+
21
+ Typeguard.configure do |config|
22
+ config.enabled = true # does nothing if false
23
+ config.source = :yard # :yard or :rbs
24
+ config.target = ['bin/example.rb'] # signatures file/dir
25
+ config.reparse = true # reparse YARD sigs
26
+ config.at_exit_report = true # print findings
27
+ config.resolution.raise_on_name_error = false # undefined constants
28
+ config.wrapping.raise_on_unexpected_arity = false # amount of parameters
29
+ config.wrapping.raise_on_unexpected_visibility = false # scope (public/private/..)
30
+ config.validation.raise_on_unexpected_argument = false # type check args
31
+ config.validation.raise_on_unexpected_return = false # type check return
32
+ end.process!
33
+ ```
34
+ ## Development
35
+
36
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
37
+
38
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
39
+
40
+ ## Contributing
41
+
42
+ Bug reports and pull requests are welcome on GitHub at https://github.com/tesorion/typeguard.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'test'
8
+ t.libs << 'lib'
9
+ t.test_files = FileList['test/**/test_*.rb']
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-configurable'
4
+
5
+ module Typeguard
6
+ extend Dry::Configurable
7
+
8
+ SUPPORTED_SOURCES = %i[yard rbs].freeze
9
+
10
+ def self.setting_bool(target = self, name, default: false)
11
+ target.setting name, default: default, reader: true, constructor: proc { |value|
12
+ raise "Config '#{name}' must be true or false" unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
13
+
14
+ value
15
+ }
16
+ end
17
+
18
+ setting_bool :enabled
19
+ setting_bool :reparse
20
+ setting_bool :at_exit_report
21
+
22
+ setting :target, reader: true
23
+ setting :source, default: :yard, reader: true, constructor: proc { |value|
24
+ raise "Config source must be one of #{SUPPORTED_SOURCES}" unless SUPPORTED_SOURCES.include?(value)
25
+
26
+ value
27
+ }
28
+
29
+ setting :resolution, reader: true do
30
+ Typeguard.setting_bool self, :raise_on_name_error
31
+ end
32
+
33
+ setting :wrapping, reader: true do
34
+ Typeguard.setting_bool self, :raise_on_unexpected_arity
35
+ Typeguard.setting_bool self, :raise_on_unexpected_visibility
36
+ end
37
+
38
+ setting :validation, reader: true do
39
+ Typeguard.setting_bool self, :raise_on_unexpected_argument
40
+ Typeguard.setting_bool self, :raise_on_unexpected_return
41
+ end
42
+
43
+ def self.process!
44
+ unless config.enabled
45
+ puts 'WARNING: typeguard disabled'
46
+ return
47
+ end
48
+
49
+ Typeguard::Metrics.config(config.validation)
50
+ Typeguard::TypeModel::Builder.send(config.source)
51
+ builder = TypeModel::Builder::IMPLEMENTATION.new(config.target, config.reparse)
52
+ definitions = builder.build
53
+ Typeguard::Resolution::Resolver.new(definitions, config.resolution).resolve!
54
+ Typeguard::Validation::Wrapper.new(definitions, config.wrapping).wrap!
55
+ at_exit { Typeguard::Metrics.flush } if config.at_exit_report
56
+ end
57
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typeguard
4
+ module Metrics
5
+ Log = Struct.new(:module, :definition, :type, :error, :expected, :actual, :source, :caller, keyword_init: true)
6
+
7
+ @raise_on_unexpected_argument = false
8
+ @raise_on_unexpected_return = false
9
+ @logs = []
10
+
11
+ def self.config(validation)
12
+ @raise_on_unexpected_argument = validation.raise_on_unexpected_argument
13
+ @raise_on_unexpected_return = validation.raise_on_unexpected_return
14
+ end
15
+
16
+ def self.format_log(log)
17
+ <<~MESSAGE
18
+ - #{log.error.upcase} - Expected #{log.expected} for #{log.type} but received incompatible \
19
+ #{log.actual} in '#{log.module}##{log.definition}' defined in #{log.source} \
20
+ and called from #{log.caller}
21
+ MESSAGE
22
+ end
23
+
24
+ def self.flush
25
+ new_line = "\n" unless @logs.empty?
26
+ puts "\ntypeguard errors [start]: #{@logs.length} #{new_line}\n"
27
+ @logs.each { |log| puts format_log(log) }
28
+ puts "\ntypeguard errors [end]: #{@logs.length} #{new_line}"
29
+ @logs.clear
30
+ end
31
+
32
+ def self.report(mod, definition, error, expected, actual)
33
+ caller = caller_locations(1, 1).first
34
+ caller_string = caller.label.split('::').last
35
+ module_name = mod.name.to_sym
36
+ type = definition.class.name.split('::').last.gsub('Definition', '').to_sym
37
+ source = definition.source
38
+ log = Log.new(module: module_name, definition: definition.name, type: type, error: error,
39
+ expected: expected, actual: actual, source: source,
40
+ caller: caller_string)
41
+ @logs << log
42
+ log
43
+ end
44
+
45
+ def self.report_unexpected_return(sig, return_object, result, mod_name)
46
+ caller = caller_locations(2, 1).first
47
+ caller_string = "#{caller.path}:#{caller.lineno}"
48
+ source = sig.returns.source
49
+ log = Log.new(module: mod_name, definition: sig.name, type: :Return,
50
+ error: :unexpected_return, source: source, caller: caller_string,
51
+ expected: return_object, actual: result.class.to_s)
52
+ @logs << log
53
+ raise TypeError, format_log(log) if @raise_on_unexpected_return
54
+
55
+ log
56
+ end
57
+
58
+ def self.report_unexpected_argument(sig, expected, actual, mod_name, parameter)
59
+ caller = caller_locations(4, 1).first
60
+ caller_string = "#{caller.path}:#{caller.lineno}"
61
+ method_name = sig.name
62
+ parameter_name = parameter.name
63
+ source = parameter.source
64
+ log = Log.new(module: mod_name, definition: method_name, type: parameter_name,
65
+ error: :unexpected_argument, source: source, caller: caller_string,
66
+ expected: expected, actual: actual.class.to_s)
67
+ @logs << log
68
+ raise TypeError, format_log(log) if @raise_on_unexpected_argument
69
+
70
+ log
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typeguard
4
+ module Resolution
5
+ class Resolver
6
+ include Typeguard::TypeModel::Definitions
7
+
8
+ def initialize(definitions, config)
9
+ @definitions = definitions
10
+ @config = config
11
+ end
12
+
13
+ def resolve!
14
+ if @config.raise_on_name_error
15
+ @definitions.each do |definition|
16
+ resolve_definition(definition)
17
+ rescue NameError => e
18
+ raise(e.class,
19
+ Metrics.format_log(Metrics.report(Object, definition, :unresolved, 'resolution', e.message)),
20
+ [])
21
+ end
22
+ else
23
+ # Create compact array of resolved definitions
24
+ resolve_prune_definitions!
25
+ end
26
+ end
27
+
28
+ def resolve_definition(definition)
29
+ case definition
30
+ when ModuleDefinition, ClassDefinition
31
+ Object.const_get(definition.name.to_s, true)
32
+ definition.children.each { |child| resolve_definition(child) }
33
+ when MethodDefinition
34
+ definition.parameters.each { |param| param.types.each { |node| resolve_type(node) } }
35
+ return if definition.name == :initialize # Ignore YARD default tag
36
+
37
+ definition.returns.types.each { |node| resolve_type(node) }
38
+ else raise "Unexpected definition for '#{definition}'"
39
+ end
40
+ end
41
+
42
+ def resolve_prune_definitions!(definitions = @definitions, parent = Object)
43
+ definitions.grep_v(MethodDefinition) do |definition|
44
+ definition.children = resolve_prune_definitions!(definition.children, definition)
45
+ end
46
+ definitions.reject do |definition|
47
+ case definition
48
+ when ModuleDefinition, ClassDefinition
49
+ Object.const_get(definition.name.to_s, true)
50
+ when MethodDefinition
51
+ definition.parameters.each { |param| param.types.each { |node| resolve_type(node) } }
52
+ next if definition.name == :initialize # Ignore YARD default tag
53
+
54
+ definition.returns.types.each { |node| resolve_type(node) }
55
+ end
56
+ false
57
+ rescue NameError => e
58
+ Metrics.report(parent, definition, :unresolved, 'resolution', e)
59
+ true
60
+ end
61
+ end
62
+
63
+ # Raises a NameError if module names are not found
64
+ def resolve_type(node)
65
+ case node.shape
66
+ when :basic
67
+ node.metadata[:const] ||= Object.const_get(node.kind.to_s, true)
68
+ when :generic, :fixed
69
+ node.metadata[:const] ||= Object.const_get(node.kind.to_s, true)
70
+ node.children.each { |child_node| resolve_type(child_node) }
71
+ when :hash, :fixed_hash
72
+ node.metadata[:const] ||= Object.const_get(node.kind.to_s, true)
73
+ node.children.flatten.each { |child_node| resolve_type(child_node) }
74
+ when :union
75
+ node.children.each { |child_node| resolve_type(child_node) }
76
+ when :literal
77
+ # The mapper has already rejected invalid literals,
78
+ when :duck
79
+ # Resolving duck-types at this point is unreliable
80
+ # and slow, leaving it up to runtime checks
81
+ when :untyped
82
+ # Always correct
83
+ else raise "Unknown node shape: #{node.shape}"
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rbs'
4
+
5
+ module Typeguard
6
+ module TypeModel
7
+ module Builder
8
+ # Takes RBS signatures and returns a generic type model
9
+ class RBSBuilder
10
+ include Typeguard::TypeModel::Definitions
11
+
12
+ attr_reader :rbs_env
13
+
14
+ # @return [Typeguard::Initializer::RBSInitalizer] initializer for RBS signatures
15
+ def initialize(target, _reparse)
16
+ rbs_loader = RBS::EnvironmentLoader.new(core_root: nil)
17
+ rbs_loader.add(path: Pathname(target))
18
+ @rbs_env = RBS::Environment.from_loader(rbs_loader).resolve_type_names
19
+ return unless @rbs_env.declarations.empty?
20
+
21
+ puts "WARNING: could not find RBS signatures for target directory '#{target}'. " \
22
+ 'Confirm that the directory exists and/or that it contains .rbs files.'
23
+ end
24
+
25
+ # ruby -e "require 'rbs';loader=RBS::EnvironmentLoader.new(core_root: nil);loader.add(path:Pathname('sig'));environment=RBS::Environment.from_loader(loader);environment.declarations.each{|cls,entries|pp cls;pp entries}"
26
+ def build
27
+ @rbs_env.declarations.map { |decl| build_object(decl) }.compact
28
+ end
29
+
30
+ def build_object(object)
31
+ case object
32
+ when RBS::AST::Declarations::Class
33
+ children = object.members.map { |child| build_object(child) }.compact
34
+ ClassDefinition.new(
35
+ name: object.name.relative!.to_s,
36
+ source: build_source(object),
37
+ parent: object.super_class&.to_s,
38
+ type_parameters: object.type_params.map(&:name),
39
+ children: children
40
+ )
41
+ when RBS::AST::Declarations::Module
42
+ children = object.members.map { |child| build_object(child) }.compact
43
+ ModuleDefinition.new(
44
+ name: object.name.relative!.to_s,
45
+ source: build_source(object),
46
+ type_parameters: object.type_params.map(&:name),
47
+ children: children
48
+ )
49
+ when RBS::AST::Members::MethodDefinition
50
+ # NOTE: Currently only looking at first overload and
51
+ # only at required positionals.
52
+ sig = object.overloads.first.method_type.type
53
+ parameters = sig.required_positionals.map do |param|
54
+ ParameterDefinition.new(
55
+ name: param.name,
56
+ source: build_source(param),
57
+ types: build_types(param.type),
58
+ types_string: param.type.to_s
59
+ )
60
+ end
61
+ return_sig = sig.return_type
62
+ returns = ReturnDefinition.new(
63
+ source: build_source(return_sig),
64
+ types: build_types(return_sig),
65
+ types_string: return_sig.to_s
66
+ )
67
+ MethodDefinition.new(
68
+ name: object.name,
69
+ source: build_source(object),
70
+ scope: object.kind,
71
+ visibility: object.visibility,
72
+ parameters: parameters,
73
+ returns: returns
74
+ )
75
+ else raise "Unsupported RBS declaration #{object.class}"
76
+ end
77
+ end
78
+
79
+ def build_types(rbs_type)
80
+ if rbs_type
81
+ [Typeguard::TypeModel::Mapper::RBSMapper.parse_map(rbs_type)]
82
+ else
83
+ [TypeNode.new(
84
+ kind: :untyped,
85
+ shape: :untyped,
86
+ children: [],
87
+ metadata: { note: 'RBS type not provided' }
88
+ )]
89
+ end
90
+ end
91
+
92
+ def build_source(object)
93
+ location = object.location
94
+ "#{location.buffer.name}:#{location.start_line}"
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end