dim-toolkit 2.1.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.
data/lib/dim/loader.rb ADDED
@@ -0,0 +1,581 @@
1
+ require 'yaml'
2
+ require 'pathname'
3
+
4
+ require_relative 'encoding'
5
+ require_relative 'globals'
6
+ require_relative 'ext/psych'
7
+ require_relative 'requirement'
8
+ require_relative 'consistency'
9
+ require_relative 'exit_helper'
10
+ require_relative 'helpers/attribute_helper'
11
+ require_relative 'helpers/file_helper'
12
+
13
+ using Dim::Refinements
14
+
15
+ module Dim
16
+ class Loader
17
+ include Helpers::AttributeHelper
18
+
19
+ attr_reader :requirements, :config, :property_table, :properties, :original_data, :module_data, :metadata, :dim_file, :all_attributes, :custom_attributes
20
+
21
+ # YAML standard:
22
+ # invalid C0 control characters: 0x00 - 0x1F (except TAB 0x09, LF 0x0A and CR 0x0D)
23
+ # invalid control character DEL 0x7F
24
+ # invalid C1 control characters: 0x80 - 0x9F (except NEL 0x85)
25
+ #
26
+ # in addition NEL will be also replaced which seems to be misused in CRS documents
27
+ @@invalid_ccs = {
28
+ "\x00" => '[NUL]', "\x01" => '[SOH]', "\x02" => '[STX]', "\x03" => '[ETX]',
29
+ "\x04" => '[EOT]', "\x05" => '[ENQ]', "\x06" => '[ACK]', "\x07" => '[BEL]',
30
+ "\x08" => '[BS]', "\x0B" => '[VT]',
31
+ "\x0C" => '[FF]', "\x0E" => '[SO]', "\x0F" => '[SI]',
32
+ "\x10" => '[DLE]', "\x11" => '[DC1]', "\x12" => '[DC2]', "\x13" => '[DC3]',
33
+ "\x14" => '[DC4]', "\x15" => '[NAK]', "\x16" => '[SYN]', "\x17" => '[ETB]',
34
+ "\x18" => '[CAN]', "\x19" => '[EM]', "\x1A" => '[SUB]', "\x1B" => '[ESC]',
35
+ "\x1C" => '[FS]', "\x1D" => '[GS]', "\x1E" => '[RS]', "\x1F" => '[US]',
36
+ "\x7F" => '[DEL]',
37
+ "\u0080" => '[PAD]', "\u0081" => '[HOP]', "\u0082" => '[BPH]', "\u0083" => '[NBH]',
38
+ "\u0084" => '[IND]', "\u0085" => '[NEL]', "\u0086" => '[SSA]', "\u0087" => '[ESA]',
39
+ "\u0088" => '[HTS]', "\u0089" => '[HTJ]', "\u008A" => '[VTS]', "\u008B" => '[PLD]',
40
+ "\u008C" => '[PLU]', "\u008D" => '[RI]', "\u008E" => '[SS2]', "\u008F" => '[SS3]',
41
+ "\u0090" => '[DCS]', "\u0091" => '[PU1]', "\u0092" => '[PU2]', "\u0093" => '[STS]',
42
+ "\u0094" => '[CCH]', "\u0095" => '[MW]', "\u0096" => '[SPA]', "\u0097" => '[EPA]',
43
+ "\u0098" => '[SOS]', "\u0099" => '[SGCI]', "\u009A" => '[SCI]', "\u009B" => '[CSI]',
44
+ "\u009C" => '[ST]', "\u009D" => '[OSC]', "\u009E" => '[PM]', "\u009F" => '[APC]'
45
+ }
46
+
47
+ def initialize
48
+ @requirements = {}
49
+ @module_data = {}
50
+ @metadata = {}
51
+ @config = {}
52
+ @properties = {}
53
+ @property_table = {}
54
+ @original_data = {}
55
+ @all_attributes = Requirement::SYNTAX.dup
56
+ @custom_attributes = {}
57
+ end
58
+
59
+ def filter(str)
60
+ @requirements.keep_if { |_id, r| r.filter(str) }
61
+ end
62
+
63
+ def load(file: nil, attributes_file: nil, allow_missing: false, no_check_enclosed: false, silent: true, input_filenames: [])
64
+ ::Psych::Nodes::Scalar.add_patch
65
+ ::Psych::Visitors::ToRuby.add_patch
66
+
67
+ input_filenames = *input_filenames
68
+ # If output format is vim then we do not need to read the file,
69
+ # content will be read from the stdin and printed out to stdout. This is to enhance the
70
+ # formatting in the vim.
71
+ unless OPTIONS[:output_format] == 'stdout'
72
+ if input_filenames.length > 0 and file
73
+ Dim::ExitHelper.exit(code: 1, msg: 'use either file or input_filenames argument (deprecated) for load method')
74
+ end
75
+ if input_filenames.length > 1
76
+ Dim::ExitHelper.exit(code: 1,
77
+ msg: 'input_filenames argument (deprecated) of load method must have at maximum one entry')
78
+ end
79
+ file = input_filenames[0] if input_filenames.length == 1
80
+ unless file
81
+ Dim::ExitHelper.exit(code: 1,
82
+ msg: 'neither file nor input_filenames argument (deprecated) argument specified for load method')
83
+ end
84
+ end
85
+
86
+ unless silent
87
+ if allow_missing && OPTIONS[:subcommand] != 'format'
88
+ puts "Warning: Using 'allow-missing' might influence metrics when some references are ignored!\n"
89
+ end
90
+ puts 'Start Loading...'
91
+ end
92
+
93
+ Dim::ExitHelper.exit(code: 1, filename: file, msg: 'does not exist') unless File.exist?(file.to_s) || OPTIONS[:output_format] == 'stdout'
94
+
95
+ @dim_file = OPTIONS[:output_format] == 'stdout' ? $stdin.read.chomp : File.binread(file).chomp
96
+
97
+ if attributes_file
98
+ fetch_attributes!(folder: File.dirname(attributes_file), filename: attributes_file.split('/').last, silent: silent)
99
+ elsif !dim_file.match(/^Config:/)
100
+ folder = search_attributes_file(file.to_s)
101
+ fetch_attributes!(folder: folder, filename: 'attributes.dim', silent: silent) if folder
102
+ end
103
+
104
+ if dim_file.match(/^Config:/)
105
+ load_config(config_filename: file.to_s, silent: silent)
106
+ else
107
+ load_pattern(
108
+ config_filename: nil,
109
+ pattern: file.to_s,
110
+ origin: '',
111
+ silent: silent,
112
+ category: 'unspecified',
113
+ disable_naming_convention_check: false,
114
+ no_check_enclosed: no_check_enclosed
115
+ )
116
+ end
117
+
118
+ puts 'Checking consistency...' unless silent
119
+ checker = Dim::Consistency.new(self)
120
+ checker.check(allow_missing: allow_missing)
121
+ puts 'Done.' unless silent
122
+ ensure
123
+ ::Psych::Nodes::Scalar.revert_patch
124
+ ::Psych::Visitors::ToRuby.revert_patch
125
+ end
126
+
127
+ def load_config(config_filename:, silent: true, no_check_enclosed: false)
128
+ puts "Loading [config] #{config_filename}..." unless silent
129
+ @config = open_yml_file(config_filename, '')
130
+
131
+ allowed_attributes = %w[Config Properties Attributes]
132
+
133
+ @config.each_key do |k|
134
+ next unless allowed_attributes.none? { |a| a == k }
135
+
136
+ Dim::ExitHelper.exit(code: 1, filename: config_filename, msg: "top level key in config file must be #{allowed_attributes.map do |a|
137
+ "\"#{a}\""
138
+ end.join(', ')}, found \"#{k}\"")
139
+ end
140
+
141
+ if @config.key?('Attributes')
142
+ if @config['Attributes'].is_a?(String)
143
+ fetch_attributes!(folder: File.dirname(config_filename), filename: @config['Attributes'], silent: silent)
144
+ else
145
+ Dim::ExitHelper.exit(code: 1, filename: config_filename, msg: "'Attributes' must be a string")
146
+ end
147
+ end
148
+
149
+ if @config.key?('Properties')
150
+ if @config['Properties'].is_a?(String)
151
+ resolve_properties(folder: File.dirname(config_filename), properties_filename: @config['Properties'])
152
+ else
153
+ Dim::ExitHelper.exit(code: 1, filename: config_filename, msg: "'Properties' must be a string")
154
+ end
155
+ end
156
+
157
+ # COP: Move to constants
158
+ allowed_keys = %w[files category originator disable_naming_convention_check]
159
+ allowed_categories = ALLOWED_CATEGORIES.values
160
+
161
+ if @config['Config'].is_a?(Array)
162
+ config_values = @config['Config']
163
+ else
164
+ Dim::ExitHelper.exit(code: 1, filename: config_filename, msg: "'Config' must be an array")
165
+ end
166
+
167
+ config_values.each do |value|
168
+ validate_and_load_config(value, config_filename)
169
+
170
+ unless value.is_a?(Hash) && value.keys.sort_by(&:length).eql?(allowed_keys.sort_by(&:length))
171
+ Dim::ExitHelper.exit(code: 1, filename: config_filename,
172
+ msg: "each hash in 'Config' array must have key/value pairs for #{allowed_keys.join(', ')}.")
173
+ end
174
+
175
+ if value['category'].is_a?(String)
176
+ value['category'].strip!
177
+ unless allowed_categories.include?(value['category'])
178
+ Dim::ExitHelper.exit(code: 1, filename: config_filename,
179
+ msg: "attribute \"category\" of '#{value['originator']}' reqs is '#{value['category']}' but must be one of #{allowed_categories.join(', ')}.")
180
+ end
181
+ else
182
+ Dim::ExitHelper.exit(code: 1, filename: config_filename,
183
+ msg: "attribute \"category\" of '#{value['originator']}' reqs must be a string")
184
+ end
185
+
186
+ if value['files'].is_a?(Array) && value['files'].all? { |a| a.is_a?(String) } || value['files'].is_a?(String)
187
+ value['files'] = *value['files']
188
+ else
189
+ Dim::ExitHelper.exit(code: 1, filename: config_filename,
190
+ msg: "attribute \"files\" of '#{value['originator']}' reqs must be a string or an array of strings.")
191
+ end
192
+ value['files'].each do |pattern|
193
+ pattern.gsub!(%r{\A\./}, '')
194
+ if pattern.empty?
195
+ Dim::ExitHelper.exit(code: 1, filename: config_filename,
196
+ msg: "attribute \"files\" of '#{value['originator']}' must not have an empty string")
197
+ end
198
+ p = Pathname.new(pattern)
199
+ if p.absolute?
200
+ Dim::ExitHelper.exit(code: 1, filename: config_filename,
201
+ msg: "'#{pattern}' must not be an absolute path")
202
+ end
203
+ if p.each_filename.any? { |e| e == '..' }
204
+ Dim::ExitHelper.exit(code: 1, filename: config_filename,
205
+ msg: "'#{pattern}' must not include '..'")
206
+ end
207
+ load_pattern(
208
+ config_filename: config_filename,
209
+ pattern: pattern,
210
+ origin: value['originator'],
211
+ silent: silent,
212
+ category: value['category'],
213
+ disable_naming_convention_check: value['disable_naming_convention_check'],
214
+ no_check_enclosed: no_check_enclosed
215
+ )
216
+ end
217
+ end
218
+ puts 'Done.' unless silent
219
+ end
220
+
221
+ def extract_type(attr)
222
+ return {} if attr.empty?
223
+
224
+ hscan = attr.scan(/\Ah(\d+)\s.+/)
225
+ if hscan.length == 1
226
+ level = hscan[0][0].to_i
227
+ return { 'type' => "heading_#{level}", 'text' => attr[2 + hscan[0][0].length..-1].strip }
228
+ else
229
+ iscan = attr.scan(/\Ainfo\s.+/)
230
+ return { 'type' => 'information', 'text' => attr[5..-1].strip } if iscan.length == 1
231
+ end
232
+ nil
233
+ end
234
+
235
+ def load_file(filename:, origin:, silent:, category:, disable_naming_convention_check: false, no_check_enclosed: false)
236
+ puts "Loading [#{origin.empty? ? "unknown" : origin}] #{filename}..." unless silent
237
+ binary_data = OPTIONS[:output_format] == 'stdout' ? @dim_file : File.binread(filename).force_encoding('UTF-8')
238
+
239
+ # this looks expensive but measurement showed it's close to zero
240
+ @@invalid_ccs.each { |k, v| binary_data = binary_data.gsub(k, v) }
241
+
242
+ if binary_data.valid_encoding?
243
+ begin
244
+ psych_doc = YAML.parse(binary_data)
245
+ rescue Psych::SyntaxError => e
246
+ Dim::ExitHelper.exit(code: 1, filename: filename, msg: e.message)
247
+ end
248
+ else
249
+ begin
250
+ psych_doc = YAML.parse(binary_data.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
251
+ filename: filename)
252
+ rescue Psych::SyntaxError => e
253
+ Dim::ExitHelper.exit(code: 1, filename: filename, msg: e.message)
254
+ end
255
+ end
256
+ unless psych_doc
257
+ puts "Warning: empty file detected; skipped loading of #{filename}"
258
+
259
+ return
260
+ end
261
+ reqs = psych_doc.to_ruby
262
+
263
+ unless reqs.is_a?(Hash)
264
+ Dim::ExitHelper.exit(code: 1, filename: filename,
265
+ msg: 'top level must be a hash with keys "module", "enclosed", "metadata" and/or unique ids'
266
+ )
267
+ end
268
+
269
+ # TODO: Remove module backward compatibility in future version
270
+ if reqs.key?('document') && reqs.key?('module')
271
+ Dim::ExitHelper.exit(
272
+ code: 1,
273
+ filename: filename,
274
+ msg: 'module and document found in the file; please rename module to document'
275
+ )
276
+ end
277
+
278
+ if reqs.key?('document')
279
+ if !reqs['document'].is_a?(String) || reqs['document'].empty?
280
+ Dim::ExitHelper.exit(code: 1, filename: filename, msg: 'document must be a non-empty string')
281
+ end
282
+ document = reqs['document']
283
+ # TODO: Remove module backward compatibility in future version
284
+ reqs['module'] = document
285
+ elsif reqs.key?('module')
286
+ if !(reqs['module'].is_a? String) || reqs['module'].empty?
287
+ Dim::ExitHelper.exit(code: 1, filename: filename, msg: 'module name must be a non-empty string')
288
+ end
289
+ document = reqs['module']
290
+ reqs['document'] = document
291
+ else
292
+ Dim::ExitHelper.exit(code: 1, filename: filename, msg: 'Document name is missing; please add document name')
293
+ end
294
+
295
+ validate_srs_name(document, disable_naming_convention_check, category, filename)
296
+
297
+ if @module_data.key?(document)
298
+ if @module_data[document][:origin] != origin
299
+ Dim::ExitHelper.exit(code: 1, filename: filename, msg:
300
+ "files of the same module must have the same owner:\n" +
301
+ "- #{@module_data[document][:files].first[0]} (#{@module_data[document][:origin]})\n" +
302
+ "- #{filename} (#{origin})")
303
+ elsif @module_data[document][:category] != category
304
+ Dim::ExitHelper.exit(code: 1, filename: filename, msg:
305
+ "files of the same module must have the same category:\n" +
306
+ "- #{@module_data[document][:files].first[0]} (#{@module_data[document][:category]})\n" +
307
+ "- #{filename} (#{category})")
308
+ end
309
+ else
310
+ @module_data[document] = { origin: origin, category: category, files: {} }
311
+ end
312
+
313
+ if @module_data[document][:files].key?(filename)
314
+ Dim::ExitHelper.exit(code: 1, filename: filename, msg: "file #{filename} already loaded")
315
+ end
316
+
317
+ @module_data[document][:files][filename] = []
318
+ @metadata[document] ||= ''
319
+
320
+ if reqs.key?('metadata')
321
+ unless reqs['metadata'].is_a?(String)
322
+ Dim::ExitHelper.exit(code: 1, filename: filename, msg: 'metadata must be a string')
323
+ end
324
+ unless reqs['metadata'].empty?
325
+ unless @metadata[document].empty?
326
+ Dim::ExitHelper.exit(code: 1, filename: filename, msg: 'only one metadata per module allowed')
327
+ end
328
+ @metadata[document] = reqs['metadata'].strip
329
+ end
330
+ end
331
+
332
+ if reqs.key?('enclosed') && !no_check_enclosed
333
+ ecl = reqs['enclosed']
334
+ ecl = [ecl] if ecl.is_a?(String)
335
+ if !ecl.is_a?(Array) || ecl.empty? || ecl.any? { |s| !s.is_a?(String) || s.empty? }
336
+ Dim::ExitHelper.exit(code: 1, filename: filename,
337
+ msg: '"enclosed" must be a non-empty string or an array of non-empty strings')
338
+ end
339
+ # Remove superfluous ./ from path
340
+ ecl = ecl.map do |path|
341
+ if path.match?(/\\/)
342
+ puts "Warning: Backward slashes detected in filepath #{path}. Use '/' over '\\' in filepath"
343
+ path.gsub!('\\', '/')
344
+ end
345
+ Pathname.new(path).cleanpath.to_s
346
+ end
347
+ dir_file = File.dirname(filename)
348
+ ecl.each do |l|
349
+ p = Pathname.new(l)
350
+ Dim::ExitHelper.exit(code: 1, filename: filename, msg: "'#{l}' must not be an absolute path") if p.absolute?
351
+ if p.each_filename.any? do |name|
352
+ name == '..'
353
+ end
354
+ Dim::ExitHelper.exit(code: 1, filename: filename,
355
+ msg: "'#{l}' must not include '..'")
356
+ end
357
+
358
+ src = File.join(dir_file, l)
359
+ src_globbed = Dir.glob(src)
360
+ if src_globbed.empty?
361
+ Dim::ExitHelper.exit(code: 1, filename: filename,
362
+ msg: "\"#{l}\" in \"enclosed\" does not refer to any existing file")
363
+ end
364
+ end
365
+ @module_data[document][:files][filename] += ecl
366
+ end
367
+
368
+ line_numbers = psych_doc.line_numbers
369
+
370
+ reqs.each do |id, attr|
371
+ next if %w[module enclosed metadata document].include? id
372
+
373
+ if @requirements.key?(id)
374
+ Dim::ExitHelper.exit(code: 1, filename: filename, msg: "id \"#{id}\" found more than once")
375
+ end
376
+
377
+ if id.include?(',')
378
+ Dim::ExitHelper.exit(code: 1, filename: filename, msg: "Disallowed ',' found in id \"#{id}\"")
379
+ end
380
+
381
+ validate_srs_name(id, disable_naming_convention_check, category, filename, 'ID')
382
+
383
+ if attr.is_a?(String)
384
+ attr.strip!
385
+ res = extract_type(attr)
386
+ if res
387
+ attr = res
388
+ else
389
+ Dim::ExitHelper.exit(code: 1, filename: filename,
390
+ msg: "Invalid short-form in requirement \"#{id}\", valid forms are \"h<level> <text>\" or \"info <text>\"")
391
+ end
392
+ elsif attr.is_a?(Hash)
393
+ attr.keys.select do |k|
394
+ @all_attributes.key?(k) && %i[multi split].include?(@all_attributes[k][:format_style])
395
+ end.each do |k|
396
+ attr[k] = attr[k].cleanUniqString if attr[k].is_a?(String)
397
+ end
398
+ attr['tags'] = attr['tags'].cleanUniqString if attr['tags'].is_a?(String)
399
+ else
400
+ Dim::ExitHelper.exit(code: 1, filename: filename, msg: "attributes for id \"#{id}\" must be key-value pairs")
401
+ end
402
+ unless attr.key?('verification_methods')
403
+ attr['verification_methods'] = attr['test_setups'] if attr.key?('test_setups')
404
+ end
405
+ attr.each do |key, value|
406
+ unless value.is_a?(String)
407
+ Dim::ExitHelper.exit(code: 1, filename: filename,
408
+ msg: "value of attribute \"#{key}\" must be String not #{value.class}")
409
+ end
410
+ attr[key].gsub!("\u00A0", ' ')
411
+ attr[key].strip!
412
+ end
413
+
414
+ r = Requirement.new(id, document, filename, attr, origin, self, category, line_numbers[id], @all_attributes)
415
+ reqs[id] = attr
416
+ @requirements[id] = r
417
+ end
418
+
419
+ @original_data[filename] = Marshal.load(Marshal.dump(reqs))
420
+ end
421
+
422
+ def load_pattern(config_filename:, pattern:, origin:, silent:, category:, disable_naming_convention_check: false, no_check_enclosed: false)
423
+ if pattern.match?(/\\/)
424
+ puts "Warning: Backward slashes detected in pattern #{pattern}. Use '/' over '\\'"
425
+ pattern.gsub!('\\', '/')
426
+ end
427
+ pattern_search = config_filename ? File.join(File.dirname(config_filename), pattern) : pattern
428
+ fs = Dir.glob(pattern_search).sort
429
+ if fs.empty? && !silent
430
+ puts "Info: no matches for \"#{pattern}\" in \"#{config_filename}\""
431
+ end
432
+ fs.each do |f|
433
+ load_file(filename: f, origin: origin, silent: silent, category: category, disable_naming_convention_check: disable_naming_convention_check, no_check_enclosed: no_check_enclosed)
434
+ end
435
+ return unless OPTIONS[:output_format] == 'stdout'
436
+
437
+ load_file(filename: '', origin: origin, silent: silent, category: category, disable_naming_convention_check: disable_naming_convention_check, no_check_enclosed: no_check_enclosed)
438
+ end
439
+
440
+ def resolve_properties(folder:, properties_filename:)
441
+ @properties = open_yml_file(folder, properties_filename, allow_empty_file: true)
442
+ unless @properties
443
+ puts "Warning: empty file detected; skipped loading of #{properties_filename}"
444
+ return
445
+ end
446
+
447
+ @properties.each do |document, value|
448
+ value.each do |attr, property_value|
449
+ next unless @all_attributes.key?(attr)
450
+
451
+ unless property_value.is_a?(String)
452
+ Dim::ExitHelper.exit(code: 1, filename: properties_filename,
453
+ msg: "The value for key #{attr} in properties files must be a string")
454
+ end
455
+
456
+ if @all_attributes.dig(attr, :allowed).nil? ||
457
+ !property_value.cleanArray.select do |val|
458
+ !@all_attributes[attr][:allowed].include?(val)
459
+ end.any?
460
+ @property_table[document] ||= {}
461
+ @property_table[document][attr] = property_value.strip
462
+ else
463
+ Dim::ExitHelper.exit(code: 1, filename: properties_filename,
464
+ msg: "The properties file includes an invalid #{attr} value '#{property_value}' for module: #{document}.")
465
+ end
466
+ end
467
+ end
468
+ end
469
+
470
+ def fetch_attributes!(folder:, filename:, silent:)
471
+ puts "Loading [attributes] #{File.join(folder, filename)}" unless silent
472
+
473
+ @custom_attributes.merge!(resolve_attributes(folder: folder, filename: filename))
474
+ @all_attributes.merge!(@custom_attributes)
475
+ end
476
+
477
+ def search_attributes_file(file)
478
+ path = Pathname.new(file).parent.realpath
479
+ if path.root?
480
+ nil
481
+ else
482
+ return path if Dir.new(path).children.include?('attributes.dim')
483
+ search_attributes_file(path)
484
+ end
485
+ end
486
+
487
+ def validate_and_load_config(value, config_filename)
488
+ disable_naming_convention_check = value['disable_naming_convention_check']
489
+ unless disable_naming_convention_check
490
+ value['disable_naming_convention_check'] = false
491
+ return
492
+ end
493
+
494
+ if value['category'] != ALLOWED_CATEGORIES[:software]
495
+ warn("Warning: disable_naming_convention_check attribute will only take effect when category is software")
496
+ end
497
+
498
+ if disable_naming_convention_check == 'yes'
499
+ value['disable_naming_convention_check'] = true
500
+ return
501
+ elsif disable_naming_convention_check == 'no'
502
+ value['disable_naming_convention_check'] = false
503
+ return
504
+ end
505
+
506
+ Dim::ExitHelper.exit(
507
+ code: 1,
508
+ filename: config_filename,
509
+ msg: 'disable_naming_convention_check in config must be either boolean value or a string "yes" or "no"'
510
+ )
511
+ end
512
+
513
+ def validate_srs_name(name, disable_naming_convention_check, category, filename, attr = 'module')
514
+ return if category != ALLOWED_CATEGORIES[:software] || disable_naming_convention_check
515
+
516
+ # raise error if not starting with SRS_
517
+ unless name.match?(/^(SRS_)/)
518
+ Dim::ExitHelper.exit(
519
+ code: 1,
520
+ filename: filename,
521
+ msg: "#{attr} #{name} in software requirement must start with \"SRS_\""
522
+ )
523
+ end
524
+
525
+ _srs, feature, aspect, *rest = name.split('_')
526
+
527
+ # Raise error if more than two _ detected
528
+ unless rest.empty?
529
+ Dim::ExitHelper.exit(
530
+ code: 1,
531
+ filename: filename,
532
+ msg: "#{attr} #{name} in software requirement must contain exactly two \"_\""
533
+ )
534
+ end
535
+
536
+ if feature.to_s.empty?
537
+ Dim::ExitHelper.exit(
538
+ code: 1,
539
+ filename: filename,
540
+ msg: "invalid #{attr} #{name} in software requirement; missing feature after \"SRS_\""
541
+ )
542
+ end
543
+
544
+ # Raise error if feature or aspect is non alphanumeric
545
+ if feature.match?(SRS_NAME_REGEX)
546
+ Dim::ExitHelper.exit(
547
+ code: 1,
548
+ filename: filename,
549
+ msg: "feature in #{attr} #{name} in software requirement contains non-alphanumeric characters"
550
+ )
551
+ end
552
+
553
+ if attr == 'module' && !aspect.to_s.empty?
554
+ Dim::ExitHelper.exit(
555
+ code: 1,
556
+ filename: filename,
557
+ msg: "invalid module #{name} in software requirement; must contain exactly one \"_\""
558
+ )
559
+ end
560
+
561
+ # Check naming convention for aspect only in case of IDs
562
+ return if attr == 'module'
563
+
564
+ if aspect.to_s.empty?
565
+ Dim::ExitHelper.exit(
566
+ code: 1,
567
+ filename: filename,
568
+ msg: "invalid ID #{name} in software requirement; missing aspect/ratio after \"SRS_feature\""
569
+ )
570
+ end
571
+
572
+ if aspect.match?(SRS_NAME_REGEX)
573
+ Dim::ExitHelper.exit(
574
+ code: 1,
575
+ filename: filename,
576
+ msg: "aspect in ID #{name} in software requirement contains non-alphanumeric characters"
577
+ )
578
+ end
579
+ end
580
+ end
581
+ end