lutaml-lml 0.3.2 → 0.3.4
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 +4 -4
- data/exe/lutaml-lml-lsp +11 -0
- data/lib/lutaml/lml/cli.rb +165 -17
- data/lib/lutaml/lml/data_processor/attribute_processing.rb +4 -2
- data/lib/lutaml/lml/data_processor/value_declaration_processing.rb +1 -0
- data/lib/lutaml/lml/data_processor.rb +8 -1
- data/lib/lutaml/lml/diagnostic.rb +149 -0
- data/lib/lutaml/lml/document_builder.rb +53 -21
- data/lib/lutaml/lml/format/adapter/standard_adapter.rb +22 -23
- data/lib/lutaml/lml/formatter/canonical.rb +719 -0
- data/lib/lutaml/lml/formatter.rb +2 -0
- data/lib/lutaml/lml/grammar/lml.artifact.json +2626 -2439
- data/lib/lutaml/lml/grammar/lml.parg +15 -4
- data/lib/lutaml/lml/grammar.rb +13 -0
- data/lib/lutaml/lml/import_resolver.rb +13 -4
- data/lib/lutaml/lml/lsp/conversions.rb +78 -0
- data/lib/lutaml/lml/lsp/keywords.rb +43 -0
- data/lib/lutaml/lml/lsp/server.rb +378 -0
- data/lib/lutaml/lml/lsp/store.rb +68 -0
- data/lib/lutaml/lml/lsp/transport.rb +53 -0
- data/lib/lutaml/lml/lsp.rb +16 -0
- data/lib/lutaml/lml/model_compiler.rb +16 -1
- data/lib/lutaml/lml/models/association.rb +3 -0
- data/lib/lutaml/lml/models/constraint.rb +3 -0
- data/lib/lutaml/lml/models/data_type.rb +3 -0
- data/lib/lutaml/lml/models/diagram.rb +3 -0
- data/lib/lutaml/lml/models/document.rb +10 -3
- data/lib/lutaml/lml/models/enum.rb +4 -1
- data/lib/lutaml/lml/models/instance.rb +3 -0
- data/lib/lutaml/lml/models/instance_collection.rb +3 -0
- data/lib/lutaml/lml/models/member_field.rb +2 -0
- data/lib/lutaml/lml/models/namespace.rb +2 -0
- data/lib/lutaml/lml/models/operation.rb +4 -1
- data/lib/lutaml/lml/models/package.rb +3 -0
- data/lib/lutaml/lml/models/primitive_type.rb +6 -3
- data/lib/lutaml/lml/models/serialization_mapping.rb +2 -0
- data/lib/lutaml/lml/models/top_element_attribute.rb +3 -0
- data/lib/lutaml/lml/models/uml_class.rb +3 -0
- data/lib/lutaml/lml/models/value.rb +3 -0
- data/lib/lutaml/lml/pipeline.rb +25 -8
- data/lib/lutaml/lml/positioned.rb +13 -0
- data/lib/lutaml/lml/preprocessor.rb +44 -13
- data/lib/lutaml/lml/quoting.rb +60 -0
- data/lib/lutaml/lml/source_position.rb +20 -0
- data/lib/lutaml/lml/symbol_index.rb +200 -0
- data/lib/lutaml/lml/transform.rb +49 -2
- data/lib/lutaml/lml/validator.rb +134 -21
- data/lib/lutaml/lml/version.rb +1 -1
- data/lib/lutaml/lml.rb +18 -1
- metadata +16 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Lml
|
|
7
|
+
module Lsp
|
|
8
|
+
# LSP base protocol framing: `Content-Length: N\r\n\r\n<json>`.
|
|
9
|
+
# Reads one message at a time and writes responses to the output
|
|
10
|
+
# stream. stdout is the protocol channel; stderr is for logging.
|
|
11
|
+
class Transport
|
|
12
|
+
def initialize(input: $stdin, output: $stdout)
|
|
13
|
+
@input = input
|
|
14
|
+
@output = output
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def read_message
|
|
18
|
+
headers = read_headers
|
|
19
|
+
return nil unless headers
|
|
20
|
+
|
|
21
|
+
length = headers["content-length"]
|
|
22
|
+
return nil unless length
|
|
23
|
+
|
|
24
|
+
body = @input.read(length.to_i)
|
|
25
|
+
body.nil? || body.empty? ? nil : JSON.parse(body)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def write_message(payload)
|
|
29
|
+
body = JSON.generate(payload)
|
|
30
|
+
@output.write("Content-Length: #{body.bytesize}\r\n\r\n#{body}")
|
|
31
|
+
@output.flush
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def read_headers
|
|
37
|
+
headers = {}
|
|
38
|
+
loop do
|
|
39
|
+
line = @input.gets
|
|
40
|
+
return nil if line.nil?
|
|
41
|
+
|
|
42
|
+
line = line.chomp
|
|
43
|
+
break if line.empty?
|
|
44
|
+
|
|
45
|
+
key, value = line.split(":", 2)
|
|
46
|
+
headers[key.to_s.strip.downcase] = value.to_s.strip
|
|
47
|
+
end
|
|
48
|
+
headers
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# Language Server Protocol support: a stdio JSON-RPC server exposing
|
|
6
|
+
# diagnostics, symbols, navigation, hover, completion, and canonical
|
|
7
|
+
# formatting over the parsed document model.
|
|
8
|
+
module Lsp
|
|
9
|
+
autoload :Transport, "lutaml/lml/lsp/transport"
|
|
10
|
+
autoload :Server, "lutaml/lml/lsp/server"
|
|
11
|
+
autoload :Store, "lutaml/lml/lsp/store"
|
|
12
|
+
autoload :Keywords, "lutaml/lml/lsp/keywords"
|
|
13
|
+
autoload :Conversions, "lutaml/lml/lsp/conversions"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -73,6 +73,17 @@ module Lutaml
|
|
|
73
73
|
return {} unless doc.instance
|
|
74
74
|
|
|
75
75
|
hydrate_instance(doc.instance)
|
|
76
|
+
rescue Lutaml::Lml::Error
|
|
77
|
+
raise
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
raise HydrationError.new(
|
|
80
|
+
"hydration failed: #{e.message}",
|
|
81
|
+
diagnostic: Diagnostic.build(
|
|
82
|
+
'hydration/LML-H01',
|
|
83
|
+
message: e.message,
|
|
84
|
+
file: input.is_a?(Document) ? input.source_path : nil,
|
|
85
|
+
),
|
|
86
|
+
)
|
|
76
87
|
end
|
|
77
88
|
|
|
78
89
|
def compiled_classes
|
|
@@ -236,7 +247,11 @@ module Lutaml
|
|
|
236
247
|
return Float::INFINITY if UNBOUNDED_TOKENS.include?(val.to_s)
|
|
237
248
|
return val.to_i if val.to_s.match?(/\A\d+\z/)
|
|
238
249
|
|
|
239
|
-
|
|
250
|
+
message = "Unrecognized cardinality token: #{val.inspect}"
|
|
251
|
+
raise ValidationError.new(
|
|
252
|
+
message,
|
|
253
|
+
diagnostic: Diagnostic.build('contract/LML-C04', message: message),
|
|
254
|
+
)
|
|
240
255
|
end
|
|
241
256
|
|
|
242
257
|
def declare_derived_fields(compiled_klass, def_obj)
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Lml
|
|
5
|
+
# A user-defined data type declaration.
|
|
5
6
|
class DataType < Lutaml::Model::Serializable
|
|
7
|
+
include Lutaml::Lml::Positioned
|
|
8
|
+
|
|
6
9
|
# From TopElement
|
|
7
10
|
attribute :name, :string
|
|
8
11
|
attribute :wire_mappings, "Lutaml::Lml::Mapping", collection: true, default: -> { [] }
|
|
@@ -2,9 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Lml
|
|
5
|
+
# The parsed LML document model.
|
|
5
6
|
class Document < Lutaml::Model::Serializable
|
|
7
|
+
include Lutaml::Lml::Positioned
|
|
8
|
+
|
|
9
|
+
# Non-serialized navigation index (definitions and uses).
|
|
10
|
+
def symbols
|
|
11
|
+
@symbols ||= SymbolIndex.new(self)
|
|
12
|
+
end
|
|
13
|
+
|
|
6
14
|
# Core attributes
|
|
7
15
|
attribute :name, :string
|
|
16
|
+
attribute :lml_version, :string
|
|
8
17
|
attribute :title, :string
|
|
9
18
|
attribute :caption, :string
|
|
10
19
|
attribute :groups, "Lutaml::Lml::Group", collection: true, default: -> { [] }
|
|
@@ -47,9 +56,7 @@ module Lutaml
|
|
|
47
56
|
def merge_instances(incoming)
|
|
48
57
|
names = @instances.collections.map(&:name)
|
|
49
58
|
incoming.collections.each do |collection|
|
|
50
|
-
if names.include?(collection.name)
|
|
51
|
-
raise Lutaml::Lml::Error, "duplicate collection name '#{collection.name}'"
|
|
52
|
-
end
|
|
59
|
+
raise Lutaml::Lml::Error, "duplicate collection name '#{collection.name}'" if names.include?(collection.name)
|
|
53
60
|
end
|
|
54
61
|
@instances.instances.concat(incoming.instances)
|
|
55
62
|
@instances.imports.concat(incoming.imports)
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Lml
|
|
5
|
+
# An enumeration declaration with its value members.
|
|
5
6
|
class Enum < Lutaml::Model::Serializable
|
|
7
|
+
include Lutaml::Lml::Positioned
|
|
8
|
+
|
|
6
9
|
# From TopElement
|
|
7
10
|
attribute :name, :string
|
|
8
11
|
attribute :definition, Lutaml::Lml::Types::TextType
|
|
@@ -15,7 +18,7 @@ module Lutaml
|
|
|
15
18
|
|
|
16
19
|
# From Enum
|
|
17
20
|
attribute :attributes, "Lutaml::Lml::TopElementAttribute", collection: true,
|
|
18
|
-
|
|
21
|
+
default: -> { [] }
|
|
19
22
|
attribute :modifier, :string
|
|
20
23
|
attribute :member_fields, "Lutaml::Lml::MemberField", collection: true, default: -> { [] }
|
|
21
24
|
attribute :from_table_name, :string
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Lml
|
|
5
|
+
# A named instances collection with its instances and imports/exports.
|
|
5
6
|
class InstanceCollection < Lutaml::Model::Serializable
|
|
7
|
+
include Lutaml::Lml::Positioned
|
|
8
|
+
|
|
6
9
|
attribute :name, :string
|
|
7
10
|
attribute :instances, "Lutaml::Lml::Instance", collection: true, default: []
|
|
8
11
|
attribute :imports, "Lutaml::Lml::InstancesImport", collection: true, default: []
|
|
@@ -5,6 +5,8 @@ module Lutaml
|
|
|
5
5
|
# A typed payload field declared on an enum:
|
|
6
6
|
# member_fields { abbreviation String urn_segment String }
|
|
7
7
|
class MemberField < Lutaml::Model::Serializable
|
|
8
|
+
include Lutaml::Lml::Positioned
|
|
9
|
+
|
|
8
10
|
attribute :name, :string
|
|
9
11
|
attribute :type, :string
|
|
10
12
|
end
|
|
@@ -6,6 +6,8 @@ module Lutaml
|
|
|
6
6
|
# The compiler generates the corresponding Lutaml::Model::XmlNamespace
|
|
7
7
|
# subclass for the compiled model namespace.
|
|
8
8
|
class Namespace < Lutaml::Model::Serializable
|
|
9
|
+
include Lutaml::Lml::Positioned
|
|
10
|
+
|
|
9
11
|
attribute :name, :string
|
|
10
12
|
attribute :uri, :string
|
|
11
13
|
attribute :prefix, :string
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Lml
|
|
5
|
+
# A class operation with parameters and a return type.
|
|
5
6
|
class Operation < Lutaml::Model::Serializable
|
|
7
|
+
include Lutaml::Lml::Positioned
|
|
8
|
+
|
|
6
9
|
# From TopElement
|
|
7
10
|
attribute :name, :string
|
|
8
11
|
attribute :definition, Lutaml::Lml::Types::TextType
|
|
@@ -17,7 +20,7 @@ module Lutaml
|
|
|
17
20
|
attribute :is_static, :boolean, default: false
|
|
18
21
|
attribute :is_abstract, :boolean, default: false
|
|
19
22
|
attribute :owned_parameter, "Lutaml::Lml::OperationParameter", collection: true,
|
|
20
|
-
|
|
23
|
+
default: -> { [] }
|
|
21
24
|
end
|
|
22
25
|
end
|
|
23
26
|
end
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Lml
|
|
5
|
+
# A primitive type declaration.
|
|
5
6
|
class PrimitiveType < Lutaml::Model::Serializable
|
|
7
|
+
include Lutaml::Lml::Positioned
|
|
8
|
+
|
|
6
9
|
# Primitives in LML are name-only entities, but the formatter
|
|
7
10
|
# accesses the same attributes as DataType via format_class.
|
|
8
11
|
attribute :name, :string
|
|
@@ -12,11 +15,11 @@ module Lutaml
|
|
|
12
15
|
attribute :visibility, :string, default: "public"
|
|
13
16
|
attribute :is_abstract, :boolean, default: false
|
|
14
17
|
attribute :attributes, "Lutaml::Lml::TopElementAttribute", collection: true,
|
|
15
|
-
|
|
18
|
+
default: -> { [] }
|
|
16
19
|
attribute :operations, "Lutaml::Lml::Operation", collection: true,
|
|
17
|
-
|
|
20
|
+
default: -> { [] }
|
|
18
21
|
attribute :associations, "Lutaml::Lml::Association", collection: true,
|
|
19
|
-
|
|
22
|
+
default: -> { [] }
|
|
20
23
|
|
|
21
24
|
def self.entity_type
|
|
22
25
|
:primitives
|
|
@@ -6,6 +6,8 @@ module Lutaml
|
|
|
6
6
|
# its class and linked by target name —
|
|
7
7
|
# mapping xml Ceramic { element "ceramic" … }
|
|
8
8
|
class SerializationMapping < Lutaml::Model::Serializable
|
|
9
|
+
include Lutaml::Lml::Positioned
|
|
10
|
+
|
|
9
11
|
attribute :format, :string
|
|
10
12
|
attribute :target, :string
|
|
11
13
|
attribute :element_name, :string
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Lml
|
|
5
|
+
# An attribute declaration or instance assignment entry.
|
|
5
6
|
class TopElementAttribute < Lutaml::Model::Serializable
|
|
7
|
+
include Lutaml::Lml::Positioned
|
|
8
|
+
|
|
6
9
|
# Core attributes
|
|
7
10
|
attribute :name, :string
|
|
8
11
|
attribute :visibility, :string, default: "public"
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Lml
|
|
5
|
+
# A class declaration with its members.
|
|
5
6
|
class UmlClass < Lutaml::Model::Serializable
|
|
7
|
+
include Lutaml::Lml::Positioned
|
|
8
|
+
|
|
6
9
|
# From TopElement. TextType unescapes \{ \} and normalizes lines
|
|
7
10
|
# on every assignment (builder, yaml, direct).
|
|
8
11
|
attribute :name, :string
|
data/lib/lutaml/lml/pipeline.rb
CHANGED
|
@@ -29,7 +29,18 @@ module Lutaml
|
|
|
29
29
|
def parse_raw(data)
|
|
30
30
|
Transform.new.apply(Parser.new.parse(data))
|
|
31
31
|
rescue Parsanol::ParseFailed => e
|
|
32
|
-
raise(
|
|
32
|
+
raise ParsingError.new(failure_message(e), diagnostic: parse_diagnostic(e))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def parse_diagnostic(error)
|
|
36
|
+
line, column = failure_location(error)
|
|
37
|
+
Diagnostic.build(
|
|
38
|
+
'parse/LML-001',
|
|
39
|
+
message: error.message.lines.first.to_s.strip,
|
|
40
|
+
file: @input.path,
|
|
41
|
+
line: line,
|
|
42
|
+
column: column,
|
|
43
|
+
)
|
|
33
44
|
end
|
|
34
45
|
|
|
35
46
|
# Parsanol releases up to 1.3.57 annotate native failures with a
|
|
@@ -38,18 +49,24 @@ module Lutaml
|
|
|
38
49
|
# rebuild the location when the marker is present.
|
|
39
50
|
def failure_message(error)
|
|
40
51
|
detail = error.message.lines.first.to_s.strip
|
|
52
|
+
line, column = failure_location(error)
|
|
53
|
+
location = (" at line #{line} char #{column}." if line && !detail.match?(/at line \d+ char \d+/))
|
|
54
|
+
"#{detail}#{location}\ncause: #{error.parse_failure_cause&.ascii_tree}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def failure_location(error)
|
|
41
58
|
cause = error.parse_failure_cause
|
|
42
59
|
offset = error.message[/@@parsanol_pos:(\d+)/, 1]&.to_i
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
end
|
|
48
|
-
"#{detail}#{location}\ncause: #{cause&.ascii_tree}"
|
|
60
|
+
return cause.source.line_and_column(offset) if offset && cause&.source
|
|
61
|
+
|
|
62
|
+
match = error.message.match(/at line (\d+) char (\d+)/)
|
|
63
|
+
match ? [match[1].to_i, match[2].to_i] : [nil, nil]
|
|
49
64
|
end
|
|
50
65
|
|
|
51
66
|
def build_document(hash)
|
|
52
|
-
DocumentBuilder.new(DocumentBuilder::DEFAULT_REGISTRY).build(:document, hash)
|
|
67
|
+
document = DocumentBuilder.new(DocumentBuilder::DEFAULT_REGISTRY).build(:document, hash)
|
|
68
|
+
document.source_path = @input.path
|
|
69
|
+
document
|
|
53
70
|
end
|
|
54
71
|
end
|
|
55
72
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# Non-serialized source metadata carried by parsed entities. These
|
|
6
|
+
# accessors are deliberately not lutaml-model attributes: positions
|
|
7
|
+
# and the source path are diagnostics metadata, not document data,
|
|
8
|
+
# and must not appear in to_hash/YAML/JSON output.
|
|
9
|
+
module Positioned
|
|
10
|
+
attr_accessor :source_position, :source_path
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Lml
|
|
5
|
+
# Expands `include` directives before parsing. Unterminated block
|
|
6
|
+
# comments are rejected here; line comments are grammar trivia.
|
|
5
7
|
class Preprocessor
|
|
6
8
|
attr_reader :input_file
|
|
7
9
|
|
|
@@ -16,7 +18,7 @@ module Lutaml
|
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def call
|
|
19
|
-
expand_lines(@input_file.read, @input_file.base_dir, [])
|
|
21
|
+
expand_lines(@input_file.read, @input_file.base_dir, [], @input_file.path)
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
private
|
|
@@ -25,24 +27,38 @@ module Lutaml
|
|
|
25
27
|
# skip (parsanol-ruby#134); only include directives are expanded
|
|
26
28
|
# here. Line structure is preserved so parse-error positions
|
|
27
29
|
# match the source.
|
|
28
|
-
def expand_lines(text, base_dir, chain)
|
|
29
|
-
validate_block_comments(text)
|
|
30
|
+
def expand_lines(text, base_dir, chain, file_path)
|
|
31
|
+
validate_block_comments(text, file_path)
|
|
30
32
|
text.split(/\r?\n/)
|
|
31
|
-
.
|
|
33
|
+
.each_with_index
|
|
34
|
+
.flat_map { |line, index| expand_line(line, base_dir, chain, file_path, index + 1) }
|
|
32
35
|
.join("\n")
|
|
33
36
|
end
|
|
34
37
|
|
|
35
|
-
def expand_line(line, base_dir, chain)
|
|
38
|
+
def expand_line(line, base_dir, chain, file_path, line_number)
|
|
36
39
|
path = include_path(line, base_dir)
|
|
37
40
|
return [line] unless path
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
if chain.include?(path)
|
|
43
|
+
raise Error.new(
|
|
44
|
+
"circular include detected: #{path}",
|
|
45
|
+
diagnostic: Diagnostic.build(
|
|
46
|
+
'parse/LML-003',
|
|
47
|
+
message: "circular include detected: #{path}",
|
|
48
|
+
file: file_path, line: line_number, column: 1
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
end
|
|
40
52
|
|
|
41
|
-
expand_lines(read_included(path
|
|
53
|
+
expand_lines(read_included(path, file_path, line_number),
|
|
54
|
+
File.dirname(path), chain + [path], path)
|
|
42
55
|
end
|
|
43
56
|
|
|
44
57
|
def include_path(line, base_dir)
|
|
45
|
-
match
|
|
58
|
+
# Linear-time match: `\S.*` cannot overlap the leading `[ \t]+`,
|
|
59
|
+
# so a line of spaces after `include` fails in O(n) rather than
|
|
60
|
+
# backtracking quadratically (CodeQL ReDoS).
|
|
61
|
+
match = line.match(/\A[ \t]*include[ \t]+(\S.*)\z/)
|
|
46
62
|
return nil unless match
|
|
47
63
|
|
|
48
64
|
File.expand_path(trim_trailing_comment(match[1].strip), base_dir)
|
|
@@ -52,17 +68,24 @@ module Lutaml
|
|
|
52
68
|
path.sub(%r{\s+(//|/\*).*}, '')
|
|
53
69
|
end
|
|
54
70
|
|
|
55
|
-
def read_included(path)
|
|
71
|
+
def read_included(path, file_path, line_number)
|
|
56
72
|
File.read(path, encoding: 'UTF-8')
|
|
57
|
-
rescue
|
|
58
|
-
raise Error
|
|
73
|
+
rescue SystemCallError => e
|
|
74
|
+
raise Error.new(
|
|
75
|
+
"cannot read include #{path}: #{e.message}",
|
|
76
|
+
diagnostic: Diagnostic.build(
|
|
77
|
+
'parse/LML-003',
|
|
78
|
+
message: "cannot read include: #{path}",
|
|
79
|
+
file: file_path, line: line_number, column: 1
|
|
80
|
+
),
|
|
81
|
+
)
|
|
59
82
|
end
|
|
60
83
|
|
|
61
84
|
# Quote-aware scan: a `/*` opened outside a string literal or a
|
|
62
85
|
# line comment must close before end of input. The grammar's
|
|
63
86
|
# trivia would otherwise reject the text with an unrelated
|
|
64
87
|
# expected-token error.
|
|
65
|
-
def validate_block_comments(text)
|
|
88
|
+
def validate_block_comments(text, file_path)
|
|
66
89
|
quote = nil
|
|
67
90
|
pos = 0
|
|
68
91
|
open_index = nil
|
|
@@ -92,7 +115,15 @@ module Lutaml
|
|
|
92
115
|
return unless open_index
|
|
93
116
|
|
|
94
117
|
line = text[0...open_index].count("\n") + 1
|
|
95
|
-
|
|
118
|
+
column = open_index - (text.rindex("\n", open_index) || -1)
|
|
119
|
+
raise Error.new(
|
|
120
|
+
"unterminated block comment at line #{line}",
|
|
121
|
+
diagnostic: Diagnostic.build(
|
|
122
|
+
'parse/LML-002',
|
|
123
|
+
message: 'unterminated block comment',
|
|
124
|
+
file: file_path, line: line, column: column
|
|
125
|
+
),
|
|
126
|
+
)
|
|
96
127
|
end
|
|
97
128
|
end
|
|
98
129
|
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# The single source of truth for scalar quoting on the emit side:
|
|
6
|
+
# bare only for runs of [A-Za-z0-9_] (the grammar's word rule);
|
|
7
|
+
# double quotes with the two RS 3001 escapes otherwise. Values,
|
|
8
|
+
# attribute keys, and the canonical form all emit through here.
|
|
9
|
+
module Quoting
|
|
10
|
+
BARE_WORD = /\A\w+\z/
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# @param scalar [String, #to_s]
|
|
15
|
+
# @return [String] the emitted form: parseable back to scalar
|
|
16
|
+
def quote_scalar(scalar)
|
|
17
|
+
str = scalar.to_s
|
|
18
|
+
return str if str.match?(BARE_WORD)
|
|
19
|
+
|
|
20
|
+
quote_always(str)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Double-quoted form even when the scalar is a bare word. Used
|
|
24
|
+
# where the grammar only accepts quoted strings (definitions,
|
|
25
|
+
# requires, instance data values, wire names).
|
|
26
|
+
#
|
|
27
|
+
# @param scalar [String, #to_s]
|
|
28
|
+
# @return [String] the double-quoted, escaped form
|
|
29
|
+
def quote_always(scalar)
|
|
30
|
+
escaped = scalar.to_s.gsub('\\') { |bs| bs * 2 }.gsub('"') { |q| "\\#{q}" }
|
|
31
|
+
"\"#{escaped}\""
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Float#to_s may use exponent notation, for which the language has
|
|
35
|
+
# no literal. Expand it to a plain decimal that parses back to the
|
|
36
|
+
# same Float.
|
|
37
|
+
#
|
|
38
|
+
# @param number [Numeric]
|
|
39
|
+
# @return [String] the emitted form
|
|
40
|
+
def number_scalar(number)
|
|
41
|
+
text = number.to_s
|
|
42
|
+
match = text.match(/\A(-?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)\z/)
|
|
43
|
+
return text unless match
|
|
44
|
+
|
|
45
|
+
integer = match[2]
|
|
46
|
+
digits = integer + match[3].to_s
|
|
47
|
+
point = integer.length + match[4].to_i
|
|
48
|
+
plain =
|
|
49
|
+
if point <= 0
|
|
50
|
+
"0.#{'0' * -point}#{digits}"
|
|
51
|
+
elsif point >= digits.length
|
|
52
|
+
"#{digits}#{'0' * (point - digits.length)}.0"
|
|
53
|
+
else
|
|
54
|
+
"#{digits[0...point]}.#{digits[point..]}"
|
|
55
|
+
end
|
|
56
|
+
"#{match[1]}#{plain}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# A position in the parsed source: 1-based line and column, with the
|
|
6
|
+
# byte offset and captured length when the parser provides them.
|
|
7
|
+
# Attached to model objects as non-serialized metadata so diagnostics
|
|
8
|
+
# can cite it without changing the document model's wire form.
|
|
9
|
+
class SourcePosition
|
|
10
|
+
attr_reader :line, :column, :offset, :length
|
|
11
|
+
|
|
12
|
+
def initialize(line:, column:, offset: nil, length: nil)
|
|
13
|
+
@line = line
|
|
14
|
+
@column = column
|
|
15
|
+
@offset = offset
|
|
16
|
+
@length = length
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|