Class: Ibex::LSP::WorkspaceAnalyzer
- Inherits:
-
Object
- Object
- Ibex::LSP::WorkspaceAnalyzer
- Defined in:
- lib/ibex/lsp/workspace_analyzer.rb,
sig/ibex/lsp/workspace_analyzer.rbs
Overview
Parses workspace sources, discovers safe include closures, and renders diagnostics.
Constant Summary collapse
- GLOB_CHARACTERS =
/[*?\[\]{}]/- WINDOWS_ABSOLUTE =
%r{\A(?:[A-Za-z]:[\\/]|\\\\)}
Instance Method Summary collapse
- #analyze(path) ⇒ analyzed_document
- #analyze_fragment(path, source) ⇒ analyzed_document
- #analyze_root(path, source) ⇒ analyzed_document
- #canonical_include(include_node) ⇒ String
- #closure(root) ⇒ [ Array[String], Hash[String, analyzed_document] ]
- #dependencies(document) ⇒ Array[String]
- #diagnostic(message, source, fallback_path) ⇒ lsp_diagnostic
- #diagnostic_parts(message, fallback_path) ⇒ [ String, Integer, Integer, String ]
- #diagnostic_range(source, line, column) ⇒ Hash[String, Hash[String, Integer]]
- #file_for_error(message, fallback) ⇒ String
- #fragment_source?(source, path) ⇒ Boolean
- #frontend_diagnostic(diagnostic, source) ⇒ lsp_diagnostic
-
#initialize(workspace, loader) ⇒ WorkspaceAnalyzer
constructor
A new instance of WorkspaceAnalyzer.
- #invalid_include(include_node) ⇒ bot
- #readable_source(path) ⇒ String
- #resolve(root) ⇒ [ Frontend::Resolution?, Array[lsp_diagnostic] ]
- #source_for_error(message, fallback) ⇒ String
Constructor Details
#initialize(workspace, loader) ⇒ WorkspaceAnalyzer
Returns a new instance of WorkspaceAnalyzer.
19 20 21 22 |
# File 'lib/ibex/lsp/workspace_analyzer.rb', line 19 def initialize(workspace, loader) @workspace = workspace @loader = loader end |
Instance Method Details
#analyze(path) ⇒ analyzed_document
25 26 27 28 29 30 31 32 33 |
# File 'lib/ibex/lsp/workspace_analyzer.rb', line 25 def analyze(path) source = @loader.read(path) return analyze_fragment(path, source) if fragment_source?(source, path) analyze_root(path, source) rescue Ibex::Error, SystemCallError => e source ||= readable_source(path) { source: source, document: nil, diagnostics: [diagnostic(e., source, path)] } end |
#analyze_fragment(path, source) ⇒ analyzed_document
74 75 76 77 |
# File 'lib/ibex/lsp/workspace_analyzer.rb', line 74 def analyze_fragment(path, source) document = Frontend::Parser.new(source, file: path, mode: :extended).parse_source_document { source: source, document: document, diagnostics: [] } end |
#analyze_root(path, source) ⇒ analyzed_document
66 67 68 69 70 71 |
# File 'lib/ibex/lsp/workspace_analyzer.rb', line 66 def analyze_root(path, source) result = Frontend::Parser.new(source, file: path, mode: :extended) .parse_with_diagnostics(max_diagnostics: 20) diagnostics = result.diagnostics.map { |entry| frontend_diagnostic(entry, source) } { source: source, document: result.document, diagnostics: diagnostics } end |
#canonical_include(include_node) ⇒ String
103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/ibex/lsp/workspace_analyzer.rb', line 103 def canonical_include(include_node) path = include_node.path return invalid_include(include_node) if path.empty? || path.include?("\0") return invalid_include(include_node) if path.start_with?("/") || path.match?(WINDOWS_ABSOLUTE) return invalid_include(include_node) if path.split(%r{[\\/]}).include?("..") return invalid_include(include_node) if path.match?(GLOB_CHARACTERS) candidate = File.(path, File.dirname(include_node.loc.file)) canonical = @loader.canonical_path(candidate, allow_missing: true) return canonical if @workspace.root_for(canonical) invalid_include(include_node) end |
#closure(root) ⇒ [ Array[String], Hash[String, analyzed_document] ]
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/ibex/lsp/workspace_analyzer.rb', line 36 def closure(root) files = [] #: Array[String] analyzed = {} #: Hash[String, analyzed_document] visiting = [root] until visiting.empty? path = visiting.pop next unless path next if analyzed.key?(path) current = analyze(path) analyzed[path] = current files << path dependencies(current.fetch(:document)).reverse_each do |target| visiting << target unless analyzed.key?(target) end end [files, analyzed] end |
#dependencies(document) ⇒ Array[String]
89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/ibex/lsp/workspace_analyzer.rb', line 89 def dependencies(document) ast = document&.ast return [] unless ast ast.declarations.filter_map do |declaration| next unless declaration.is_a?(Frontend::AST::Include) canonical_include(declaration) rescue Ibex::Error, SystemCallError nil end end |
#diagnostic(message, source, fallback_path) ⇒ lsp_diagnostic
123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/ibex/lsp/workspace_analyzer.rb', line 123 def diagnostic(, source, fallback_path) file, line, column, text = diagnostic_parts(, fallback_path) { "range" => diagnostic_range(source, line, column), "severity" => 1, "code" => "ibex.frontend", "source" => "ibex", "message" => text, "data" => { "file" => file } } end |
#diagnostic_parts(message, fallback_path) ⇒ [ String, Integer, Integer, String ]
162 163 164 165 166 167 |
# File 'lib/ibex/lsp/workspace_analyzer.rb', line 162 def diagnostic_parts(, fallback_path) match = .match(/\A(.*):(\d+):(\d+):\s*(.*)\z/m) return [fallback_path, 1, 1, ] unless match [match[1] || fallback_path, Integer(match[2]), Integer(match[3]), match[4] || ] end |
#diagnostic_range(source, line, column) ⇒ Hash[String, Hash[String, Integer]]
170 171 172 173 174 175 176 |
# File 'lib/ibex/lsp/workspace_analyzer.rb', line 170 def diagnostic_range(source, line, column) line_text = source.lines.fetch(line - 1, "").delete_suffix("\n").delete_suffix("\r") prefix = line_text.each_char.take([column - 1, 0].max).join character = prefix.each_codepoint.sum { |codepoint| codepoint > 0xFFFF ? 2 : 1 } point = { "line" => [line - 1, 0].max, "character" => character } { "start" => point, "end" => point.dup } end |
#file_for_error(message, fallback) ⇒ String
186 187 188 189 190 191 |
# File 'lib/ibex/lsp/workspace_analyzer.rb', line 186 def file_for_error(, fallback) match = .match(/\A(.*):\d+:\d+:/) return fallback unless match match[1] || fallback end |
#fragment_source?(source, path) ⇒ Boolean
80 81 82 83 84 85 86 |
# File 'lib/ibex/lsp/workspace_analyzer.rb', line 80 def fragment_source?(source, path) document, = Frontend::Lexer.new(source, file: path).tokenize_document_recovering(max_diagnostics: 1) first = document.tokens.find { |token| token.type != :eof } first&.value == "fragment" rescue Ibex::Error false end |
#frontend_diagnostic(diagnostic, source) ⇒ lsp_diagnostic
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/ibex/lsp/workspace_analyzer.rb', line 136 def frontend_diagnostic(diagnostic, source) range = if diagnostic.span PositionCodec.new(source).range(diagnostic.span) else diagnostic_range(source, diagnostic.location.line, diagnostic.location.column) end { "range" => range, "severity" => 1, "code" => diagnostic.code, "source" => "ibex", "message" => diagnostic., "data" => { "file" => diagnostic.location.file } } rescue ArgumentError, Ibex::Error { "range" => diagnostic_range("", 1, 1), "severity" => 1, "code" => diagnostic.code, "source" => "ibex", "message" => diagnostic., "data" => { "file" => diagnostic.location.file } } end |
#invalid_include(include_node) ⇒ bot
118 119 120 |
# File 'lib/ibex/lsp/workspace_analyzer.rb', line 118 def invalid_include(include_node) raise Ibex::Error, "#{include_node.loc}: unsafe include path" end |
#readable_source(path) ⇒ String
179 180 181 182 183 |
# File 'lib/ibex/lsp/workspace_analyzer.rb', line 179 def readable_source(path) @loader.read(path) rescue SystemCallError "" end |
#resolve(root) ⇒ [ Frontend::Resolution?, Array[lsp_diagnostic] ]
56 57 58 59 60 61 |
# File 'lib/ibex/lsp/workspace_analyzer.rb', line 56 def resolve(root) resolution = Frontend::Resolver.new(root, mode: :extended, loader: @loader).resolve [resolution, []] rescue Ibex::Error, SystemCallError => e [nil, [diagnostic(e., source_for_error(e., root), file_for_error(e., root))]] end |
#source_for_error(message, fallback) ⇒ String
194 195 196 |
# File 'lib/ibex/lsp/workspace_analyzer.rb', line 194 def source_for_error(, fallback) readable_source(file_for_error(, fallback)) end |