module Steep class Project class Target attr_reader :name attr_reader :options attr_reader :source_patterns attr_reader :ignore_patterns attr_reader :signature_patterns attr_reader :source_files attr_reader :signature_files attr_reader :status SignatureSyntaxErrorStatus = Struct.new(:timestamp, :errors, keyword_init: true) SignatureValidationErrorStatus = Struct.new(:timestamp, :errors, keyword_init: true) SignatureOtherErrorStatus = Struct.new(:timestamp, :error, keyword_init: true) TypeCheckStatus = Struct.new(:environment, :subtyping, :type_check_sources, :timestamp, keyword_init: true) def initialize(name:, options:, source_patterns:, ignore_patterns:, signature_patterns:) @name = name @options = options @source_patterns = source_patterns @ignore_patterns = ignore_patterns @signature_patterns = signature_patterns @source_files = {} @signature_files = {} end def add_source(path, content = "") file = SourceFile.new(path: path) if block_given? file.content = yield else file.content = content end source_files[path] = file end def remove_source(path) source_files.delete(path) end def update_source(path, content = nil) file = source_files[path] if block_given? file.content = yield(file.content) else file.content = content || file.content end end def add_signature(path, content = "") file = SignatureFile.new(path: path) if block_given? file.content = yield else file.content = content end signature_files[path] = file end def remove_signature(path) signature_files.delete(path) end def update_signature(path, content = nil) file = signature_files[path] if block_given? file.content = yield(file.content) else file.content = content || file.content end end def source_file?(path) source_files.key?(path) end def signature_file?(path) signature_files.key?(path) end def possible_source_file?(path) self.class.test_pattern(source_patterns, path, ext: ".rb") && !self.class.test_pattern(ignore_patterns, path, ext: ".rb") end def possible_signature_file?(path) self.class.test_pattern(signature_patterns, path, ext: ".rbs") end def self.test_pattern(patterns, path, ext:) patterns.any? do |pattern| p = pattern.end_with?(File::Separator) ? pattern : pattern + File::Separator p.delete_prefix!('./') (path.to_s.start_with?(p) && path.extname == ext) || File.fnmatch(pattern, path.to_s) end end def type_check(target_sources: source_files.values, validate_signatures: true) Steep.logger.tagged "target#type_check(target_sources: [#{target_sources.map(&:path).join(", ")}], validate_signatures: #{validate_signatures})" do Steep.measure "load signature and type check" do load_signatures(validate: validate_signatures) do |env, check, timestamp| Steep.measure "type checking #{target_sources.size} files" do run_type_check(env, check, timestamp, target_sources: target_sources) end end end end end def self.construct_env_loader(options:) repo = RBS::Repository.new(no_stdlib: options.vendor_path) options.repository_paths.each do |path| repo.add(path) end loader = RBS::EnvironmentLoader.new( core_root: options.vendor_path ? nil : RBS::EnvironmentLoader::DEFAULT_CORE_ROOT, repository: repo ) loader.add(path: options.vendor_path) if options.vendor_path options.libraries.each do |lib| name, version = lib.split(/:/, 2) loader.add(library: name, version: version) end loader end def environment @environment ||= RBS::Environment.from_loader(Target.construct_env_loader(options: options)) end def load_signatures(validate:) timestamp = case status when TypeCheckStatus status.timestamp end now = Time.now updated_files = [] signature_files.each_value do |file| if !timestamp || file.content_updated_at >= timestamp updated_files << file file.load!() end end if signature_files.each_value.all? {|file| file.status.is_a?(SignatureFile::DeclarationsStatus) } if status.is_a?(TypeCheckStatus) && updated_files.empty? yield status.environment, status.subtyping, status.timestamp else begin env = environment.dup signature_files.each_value do |file| if file.status.is_a?(SignatureFile::DeclarationsStatus) file.status.declarations.each do |decl| env << decl end end end env = env.resolve_type_names definition_builder = RBS::DefinitionBuilder.new(env: env) factory = AST::Types::Factory.new(builder: definition_builder) check = Subtyping::Check.new(factory: factory) if validate validator = Signature::Validator.new(checker: check) validator.validate() if validator.no_error? yield env, check, now else @status = SignatureValidationErrorStatus.new( errors: validator.each_error.to_a, timestamp: now ) end else yield env, check, Time.now end rescue RBS::DuplicatedDeclarationError => exn @status = SignatureValidationErrorStatus.new( errors: [ Signature::Errors::DuplicatedDefinitionError.new( name: exn.name, location: exn.decls[0].location ) ], timestamp: now ) rescue => exn Steep.log_error exn @status = SignatureOtherErrorStatus.new(error: exn, timestamp: now) end end else errors = signature_files.each_value.with_object([]) do |file, errors| if file.status.is_a?(SignatureFile::ParseErrorStatus) errors << file.status.error end end @status = SignatureSyntaxErrorStatus.new( errors: errors, timestamp: Time.now ) end end def run_type_check(env, check, timestamp, target_sources: source_files.values) type_check_sources = [] target_sources.each do |file| Steep.logger.tagged("path=#{file.path}") do if file.type_check(check, timestamp) type_check_sources << file end end end @status = TypeCheckStatus.new( environment: env, subtyping: check, type_check_sources: type_check_sources, timestamp: timestamp ) end def no_error? source_files.all? do |_, file| file.status.is_a?(Project::SourceFile::TypeCheckStatus) end end def errors case status when TypeCheckStatus source_files.each_value.flat_map(&:errors).select { |error | options.error_to_report?(error) } else [] end end end end end