dim-toolkit 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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