expressir 2.1.20 → 2.1.22

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0e1c52810ce3b1361d9f8327c0c270ac3a7f366f16d3b3236a0ed3e559155a9
4
- data.tar.gz: a852899cbf8d6a7747b8609ae7ec00736eddfcfad6a9fd8d410cd453811a4188
3
+ metadata.gz: fab262d4a8c290f1dc292ff8ea22e9f631356945fb76bed736594dad060078b0
4
+ data.tar.gz: 45e33a9920262038833f1f00113b12527eb9dc64a71d29d8e0b5f36bd89842ff
5
5
  SHA512:
6
- metadata.gz: f8db59aa54a5a1edd29165ec3b79e9e98a35ebd51d8d0184735417c96bbd9d178328ddad2feef11f177bc140bd514913d5581f0c01ba8f9ee25b5a3cdf35a7d2
7
- data.tar.gz: fef82dcf757767e2a1eea5b252083f0b8f9db85226263f74c7979fde768caa25895df05cca29ac525b799f21ffd19dfd138e26087d3351e0a210b62746bd1867
6
+ metadata.gz: 3bbc6a59dc02947a9c2ed5971900094ce183a4bbe0bdc4e3e762906d8d0675bc5c3ed145c37f17a406c406db783d4dcac1055938cdef2f54382bf1647bd57a61
7
+ data.tar.gz: ae2c40bc87334138a4ce2cbfdc04bf699da520f62483bf9a8760117ba00b7d45dde639e0c292b5cdb8a285c1d01bd59aef4f5bf70f976ded713c1d7fb4ec5a61
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2025-06-05 12:41:11 UTC using RuboCop version 1.75.2.
3
+ # on 2025-06-07 07:39:56 UTC using RuboCop version 1.75.2.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -13,6 +13,24 @@ Gemspec/RequiredRubyVersion:
13
13
  Exclude:
14
14
  - 'expressir.gemspec'
15
15
 
16
+ # Offense count: 2
17
+ # This cop supports safe autocorrection (--autocorrect).
18
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
19
+ # SupportedStyles: special_inside_parentheses, consistent, align_braces
20
+ Layout/FirstHashElementIndentation:
21
+ Exclude:
22
+ - 'spec/expressir/commands/coverage_ignore_files_spec.rb'
23
+
24
+ # Offense count: 1
25
+ # This cop supports safe autocorrection (--autocorrect).
26
+ # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
27
+ # SupportedHashRocketStyles: key, separator, table
28
+ # SupportedColonStyles: key, separator, table
29
+ # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
30
+ Layout/HashAlignment:
31
+ Exclude:
32
+ - 'spec/expressir/commands/coverage_ignore_files_spec.rb'
33
+
16
34
  # Offense count: 2
17
35
  # This cop supports safe autocorrection (--autocorrect).
18
36
  # Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
@@ -41,7 +59,7 @@ Lint/UnusedMethodArgument:
41
59
  - 'lib/expressir/express/cache.rb'
42
60
  - 'lib/expressir/express/parser.rb'
43
61
 
44
- # Offense count: 75
62
+ # Offense count: 79
45
63
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
46
64
  Metrics/AbcSize:
47
65
  Exclude:
@@ -60,13 +78,13 @@ Metrics/AbcSize:
60
78
  - 'lib/expressir/model/declarations/schema.rb'
61
79
  - 'lib/expressir/model/model_element.rb'
62
80
 
63
- # Offense count: 1
81
+ # Offense count: 2
64
82
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
65
83
  # AllowedMethods: refine
66
84
  Metrics/BlockLength:
67
85
  Max: 143
68
86
 
69
- # Offense count: 54
87
+ # Offense count: 57
70
88
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
71
89
  Metrics/CyclomaticComplexity:
72
90
  Exclude:
@@ -81,12 +99,12 @@ Metrics/CyclomaticComplexity:
81
99
  - 'lib/expressir/model/model_element.rb'
82
100
  - 'spec/support/model_element_helper.rb'
83
101
 
84
- # Offense count: 99
102
+ # Offense count: 103
85
103
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
86
104
  Metrics/MethodLength:
87
105
  Max: 82
88
106
 
89
- # Offense count: 43
107
+ # Offense count: 45
90
108
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
91
109
  Metrics/PerceivedComplexity:
92
110
  Exclude:
@@ -98,19 +116,39 @@ Metrics/PerceivedComplexity:
98
116
  - 'lib/expressir/model/declarations/schema.rb'
99
117
  - 'lib/expressir/model/model_element.rb'
100
118
 
119
+ # Offense count: 1
120
+ # Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros, UseSorbetSigs.
121
+ # NamePrefix: is_, has_, have_, does_
122
+ # ForbiddenPrefixes: is_, has_, have_, does_
123
+ # AllowedMethods: is_a?
124
+ # MethodDefinitionMacros: define_method, define_singleton_method
125
+ Naming/PredicateName:
126
+ Exclude:
127
+ - 'spec/**/*'
128
+ - 'lib/expressir/coverage.rb'
129
+
101
130
  # Offense count: 5
102
131
  Performance/FixedSize:
103
132
  Exclude:
104
133
  - 'lib/expressir/express/formatter.rb'
105
134
 
106
- # Offense count: 6
107
- # This cop supports unsafe autocorrection (--autocorrect-all).
108
- # Configuration parameters: AllowRegexpMatch.
109
- Performance/RedundantEqualityComparisonBlock:
110
- Exclude:
111
- - 'spec/expressir/coverage_spec.rb'
112
-
113
135
  # Offense count: 1
114
136
  Style/MissingRespondToMissing:
115
137
  Exclude:
116
138
  - 'lib/expressir/express/visitor.rb'
139
+
140
+ # Offense count: 1
141
+ # This cop supports safe autocorrection (--autocorrect).
142
+ # Configuration parameters: EnforcedStyleForMultiline.
143
+ # SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma
144
+ Style/TrailingCommaInArrayLiteral:
145
+ Exclude:
146
+ - 'spec/expressir/commands/coverage_ignore_files_spec.rb'
147
+
148
+ # Offense count: 1
149
+ # This cop supports safe autocorrection (--autocorrect).
150
+ # Configuration parameters: EnforcedStyleForMultiline.
151
+ # SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma
152
+ Style/TrailingCommaInHashLiteral:
153
+ Exclude:
154
+ - 'spec/expressir/commands/coverage_ignore_files_spec.rb'
data/README.adoc CHANGED
@@ -271,6 +271,7 @@ The coverage command supports different output formats and exclusion options:
271
271
  | `--format json` | Output in JSON format for programmatic processing
272
272
  | `--format yaml` | Output in YAML format for programmatic processing
273
273
  | `--exclude TYPES` | Comma-separated list of EXPRESS entity types to exclude from coverage analysis
274
+ | `--ignore-files PATH` | Path to YAML file containing array of files to ignore from overall coverage calculation
274
275
  |===
275
276
 
276
277
  ==== Excluding entity types from coverage
@@ -344,6 +345,201 @@ expressir coverage --exclude=TYPE:SELECT schemas/resources/action_schema/action_
344
345
  expressir coverage --exclude=TYPE:SELECT,TYPE:ENUMERATION schemas/resources/action_schema/action_schema.exp
345
346
  ----
346
347
 
348
+ ==== FUNCTION subtype exclusion
349
+
350
+ For FUNCTION elements, you can exclude inner functions (functions nested within
351
+ other functions, rules, or procedures) using the `FUNCTION:INNER` syntax:
352
+
353
+ [source, sh]
354
+ ----
355
+ # Exclude inner functions from coverage analysis
356
+ expressir coverage --exclude=FUNCTION:INNER schemas/resources/action_schema/action_schema.exp
357
+
358
+ # Combine with other exclusions
359
+ expressir coverage --exclude=TYPE:SELECT,FUNCTION:INNER schemas/resources/action_schema/action_schema.exp
360
+ ----
361
+
362
+ This is useful when you want to focus documentation coverage on top-level
363
+ functions while excluding nested helper functions that may not require
364
+ individual documentation. The exclusion works recursively, excluding functions
365
+ at any nesting level within other constructs.
366
+
367
+ Valid FUNCTION subtypes that can be excluded:
368
+
369
+ `INNER`:: Inner functions nested within other functions, rules, or procedures (at any depth)
370
+ +
371
+ [example]
372
+ ====
373
+ ----
374
+ FUNCTION outer_function : BOOLEAN;
375
+ -- This inner function would be excluded with FUNCTION:INNER
376
+ FUNCTION inner_helper_function : BOOLEAN;
377
+ -- Even deeply nested functions are excluded
378
+ FUNCTION deeply_nested_function : BOOLEAN;
379
+ RETURN (TRUE);
380
+ END_FUNCTION;
381
+ RETURN (TRUE);
382
+ END_FUNCTION;
383
+
384
+ RETURN (TRUE);
385
+ END_FUNCTION;
386
+
387
+ RULE example_rule FOR (some_entity);
388
+ -- Inner functions in rules are also excluded
389
+ FUNCTION inner_function_in_rule : BOOLEAN;
390
+ RETURN (TRUE);
391
+ END_FUNCTION;
392
+ WHERE
393
+ WR1: inner_function_in_rule();
394
+ END_RULE;
395
+
396
+ PROCEDURE example_procedure;
397
+ -- Inner functions in procedures are also excluded
398
+ FUNCTION inner_function_in_procedure : BOOLEAN;
399
+ RETURN (TRUE);
400
+ END_FUNCTION;
401
+ END_PROCEDURE;
402
+ ----
403
+ ====
404
+
405
+ The `FUNCTION:INNER` exclusion helps maintain focus on documenting the primary
406
+ API functions while ignoring implementation details of nested helper functions.
407
+
408
+ ==== Ignoring files from coverage calculation
409
+
410
+ You can exclude entire files from the overall coverage calculation using the
411
+ `--ignore-files` option. This is useful when you have files that should not
412
+ contribute to the overall documentation coverage statistics, such as test
413
+ schemas, example files, or legacy schemas.
414
+
415
+ [source, sh]
416
+ ----
417
+ # Use ignore files to exclude specific files from coverage calculation
418
+ expressir coverage --ignore-files ignore_list.yaml schemas/resources/
419
+
420
+ # Combine with other options
421
+ expressir coverage --ignore-files ignore_list.yaml --exclude=TYPE:SELECT --format=json schemas/resources/
422
+ ----
423
+
424
+ ===== Ignore files YAML format
425
+
426
+ The ignore files YAML should contain an array of file patterns. Each pattern
427
+ can be either an exact file path or use glob patterns for matching multiple files.
428
+
429
+ .ignore_list.yaml
430
+ [source, yaml]
431
+ ----
432
+ # Array of file patterns to ignore
433
+ - examples/test_schema.exp # Exact file path
434
+ - examples/*_test_*.exp # Glob pattern for test files
435
+ - legacy/old_*.exp # Glob pattern for legacy files
436
+ - temp/temporary_schema.exp # Another exact path
437
+ ----
438
+
439
+ ===== Pattern matching behavior
440
+
441
+ File patterns in the ignore files YAML support:
442
+
443
+ * **Exact paths**: Match specific files exactly
444
+ * **Glob patterns**: Use `*` for wildcard matching
445
+ * **Relative paths**: Patterns are resolved relative to the YAML file's directory
446
+ * **Absolute paths**: Full system paths are also supported
447
+
448
+ [source, yaml]
449
+ ----
450
+ # Examples of different pattern types
451
+ - schemas/action_schema/action_schema.exp # Exact relative path
452
+ - /full/path/to/schema.exp # Absolute path
453
+ - schemas/**/test_*.exp # Recursive glob pattern
454
+ - temp/*.exp # All .exp files in temp directory
455
+ ----
456
+
457
+ ===== Behavior of ignored files
458
+
459
+ When files are ignored using the `--ignore-files` option:
460
+
461
+ . **Excluded from overall statistics**: Ignored files do not contribute to the
462
+ overall coverage percentage calculation
463
+
464
+ . **Still processed and reported**: Ignored files are still analyzed and appear
465
+ in the output, but marked with an `ignored: true` flag
466
+
467
+ . **Separate reporting section**: In JSON/YAML output formats, ignored files
468
+ appear in both the main `files` section (with the ignored flag) and in a
469
+ separate `ignored_files` section
470
+
471
+ . **Overall statistics updated**: The overall statistics include additional
472
+ fields showing the count of ignored files and entities
473
+
474
+ .Example JSON output with ignored files:
475
+ [source, json]
476
+ ----
477
+ {
478
+ "overall": {
479
+ "coverage_percentage": 75.0,
480
+ "total_entities": 100,
481
+ "documented_entities": 75,
482
+ "undocumented_entities": 25,
483
+ "ignored_files_count": 2,
484
+ "ignored_entities_count": 15
485
+ },
486
+ "files": [
487
+ {
488
+ "file": "schemas/main_schema.exp",
489
+ "ignored": false,
490
+ "coverage": 80.0,
491
+ "total": 50,
492
+ "documented": 40,
493
+ "undocumented": ["entity1", "entity2"]
494
+ },
495
+ {
496
+ "file": "examples/test_schema.exp",
497
+ "ignored": true,
498
+ "matched_pattern": "examples/*_test_*.exp",
499
+ "coverage": 20.0,
500
+ "total": 10,
501
+ "documented": 2,
502
+ "undocumented": ["test_entity1", "test_entity2"]
503
+ }
504
+ ],
505
+ "ignored_files": [
506
+ {
507
+ "file": "examples/test_schema.exp",
508
+ "matched_pattern": "examples/*_test_*.exp",
509
+ "coverage": 20.0,
510
+ "total": 10,
511
+ "documented": 2,
512
+ "undocumented": ["test_entity1", "test_entity2"]
513
+ }
514
+ ]
515
+ }
516
+ ----
517
+
518
+ ===== Error handling
519
+
520
+ The ignore files functionality handles various error conditions gracefully:
521
+
522
+ * **Missing YAML file**: If the specified ignore files YAML doesn't exist, a
523
+ warning is displayed and coverage analysis continues normally
524
+
525
+ * **Invalid YAML format**: If the YAML file is malformed or doesn't contain an
526
+ array, a warning is displayed and the file is ignored
527
+
528
+ * **Non-matching patterns**: Patterns that don't match any files are silently
529
+ ignored (no error or warning)
530
+
531
+ * **Permission errors**: File access errors are reported as warnings
532
+
533
+ ===== Use cases for ignore files
534
+
535
+ Common scenarios where ignore files are useful:
536
+
537
+ * **Test schemas**: Exclude test or example schemas from production coverage metrics
538
+ * **Legacy files**: Ignore old schemas that are being phased out
539
+ * **Generated files**: Exclude automatically generated schemas
540
+ * **Work-in-progress**: Temporarily ignore files under development
541
+ * **Different coverage standards**: Apply different documentation standards to different file sets
542
+
347
543
  Valid TYPE subtypes that can be excluded:
348
544
 
349
545
  `AGGREGATE`:: Aggregate type
data/lib/expressir/cli.rb CHANGED
@@ -57,6 +57,7 @@ module Expressir
57
57
  method_option :format, type: :string, desc: "Output format (text, json, yaml)", default: "text"
58
58
  method_option :exclude, type: :string, desc: "Comma-separated list of EXPRESS entity types to skip from coverage (e.g., TYPE,CONSTANT,TYPE:SELECT)"
59
59
  method_option :output, type: :string, desc: "Output file path for JSON/YAML formats (defaults to coverage_report.json/yaml)"
60
+ method_option :ignore_files, type: :string, desc: "Path to YAML file containing array of files to ignore from overall coverage calculation"
60
61
  def coverage(*paths)
61
62
  Commands::Coverage.new(options).run(paths)
62
63
  end
@@ -32,27 +32,28 @@ module Expressir
32
32
 
33
33
  def collect_reports(paths)
34
34
  reports = []
35
+ ignored_files = parse_ignore_files
35
36
 
36
37
  paths.each do |path|
37
- handle_path(path, reports)
38
+ handle_path(path, reports, ignored_files)
38
39
  end
39
40
 
40
41
  reports
41
42
  end
42
43
 
43
- def handle_path(path, reports)
44
+ def handle_path(path, reports, ignored_files)
44
45
  if File.directory?(path)
45
- handle_directory(path, reports)
46
+ handle_directory(path, reports, ignored_files)
46
47
  elsif File.extname(path).downcase == ".exp"
47
- handle_express_file(path, reports)
48
+ handle_express_file(path, reports, ignored_files)
48
49
  elsif [".yml", ".yaml"].include?(File.extname(path).downcase)
49
- handle_yaml_manifest(path, reports)
50
+ handle_yaml_manifest(path, reports, ignored_files)
50
51
  else
51
52
  say "Unsupported file type: #{path}"
52
53
  end
53
54
  end
54
55
 
55
- def handle_directory(path, reports)
56
+ def handle_directory(path, reports, ignored_files)
56
57
  say "Processing directory: #{path}"
57
58
  exp_files = Dir.glob(File.join(path, "**", "*.exp"))
58
59
  if exp_files.empty?
@@ -79,26 +80,26 @@ module Expressir
79
80
  progress.increment
80
81
  end
81
82
  skip_types = parse_skip_types
82
- report = Expressir::Coverage::Report.from_repository(repository, skip_types)
83
+ report = Expressir::Coverage::Report.from_repository(repository, skip_types, ignored_files)
83
84
  reports << report
84
85
  rescue StandardError => e
85
86
  say "Error processing directory #{path}: #{e.message}"
86
87
  end
87
88
  end
88
89
 
89
- def handle_express_file(path, reports)
90
+ def handle_express_file(path, reports, ignored_files)
90
91
  say "Processing file: #{path}"
91
92
  begin
92
93
  # For a single file, we don't need a progress bar
93
94
  skip_types = parse_skip_types
94
- report = Expressir::Coverage::Report.from_file(path, skip_types)
95
+ report = Expressir::Coverage::Report.from_file(path, skip_types, ignored_files)
95
96
  reports << report
96
97
  rescue StandardError => e
97
98
  say "Error processing file #{path}: #{e.message}"
98
99
  end
99
100
  end
100
101
 
101
- def handle_yaml_manifest(path, reports)
102
+ def handle_yaml_manifest(path, reports, ignored_files)
102
103
  say "Processing YAML manifest: #{path}"
103
104
  begin
104
105
  schema_list = YAML.load_file(path)
@@ -149,7 +150,7 @@ module Expressir
149
150
 
150
151
  # Create and add the report
151
152
  skip_types = parse_skip_types
152
- report = Expressir::Coverage::Report.from_repository(repository, skip_types)
153
+ report = Expressir::Coverage::Report.from_repository(repository, skip_types, ignored_files)
153
154
  reports << report
154
155
  end
155
156
  rescue StandardError => e
@@ -258,20 +259,39 @@ module Expressir
258
259
  end
259
260
 
260
261
  def build_structured_report(reports)
261
- {
262
- "overall" => {
263
- "total_entities" => reports.sum { |r| r.total_entities.size },
264
- "documented_entities" => reports.sum { |r| r.documented_entities.size },
265
- "undocumented_entities" => reports.sum { |r| r.undocumented_entities.size },
266
- "coverage_percentage" => if reports.sum { |r| r.total_entities.size }.positive?
267
- (reports.sum { |r| r.documented_entities.size }.to_f / reports.sum { |r| r.total_entities.size } * 100).round(2)
268
- else
269
- 100.0
270
- end,
271
- },
262
+ # Calculate ignored file statistics
263
+ ignored_files = reports.flat_map(&:ignored_file_reports)
264
+ ignored_entities_count = ignored_files.sum { |f| f["total"] }
265
+
266
+ overall_stats = {
267
+ "total_entities" => reports.sum { |r| r.total_entities.size },
268
+ "documented_entities" => reports.sum { |r| r.documented_entities.size },
269
+ "undocumented_entities" => reports.sum { |r| r.undocumented_entities.size },
270
+ "coverage_percentage" => if reports.sum { |r| r.total_entities.size }.positive?
271
+ (reports.sum { |r| r.documented_entities.size }.to_f / reports.sum { |r| r.total_entities.size } * 100).round(2)
272
+ else
273
+ 100.0
274
+ end,
275
+ }
276
+
277
+ # Add ignored file information if there are any
278
+ if ignored_files.any?
279
+ overall_stats["ignored_files_count"] = ignored_files.size
280
+ overall_stats["ignored_entities_count"] = ignored_entities_count
281
+ end
282
+
283
+ structured_report = {
284
+ "overall" => overall_stats,
272
285
  "files" => reports.flat_map(&:file_reports),
273
286
  "directories" => reports.flat_map(&:directory_reports),
274
287
  }
288
+
289
+ # Add ignored files section if there are any
290
+ if ignored_files.any?
291
+ structured_report["ignored_files"] = ignored_files
292
+ end
293
+
294
+ structured_report
275
295
  end
276
296
 
277
297
  def display_json_output(reports)
@@ -307,11 +327,11 @@ module Expressir
307
327
  requested_types
308
328
  end
309
329
 
310
- # Validate a single skip type (supports TYPE:SUBTYPE syntax)
330
+ # Validate a single skip type (supports TYPE:SUBTYPE and FUNCTION:SUBTYPE syntax)
311
331
  # @param type [String] The type to validate
312
332
  def validate_skip_type(type)
313
333
  if type.include?(":")
314
- # Handle TYPE:SUBTYPE format
334
+ # Handle TYPE:SUBTYPE and FUNCTION:SUBTYPE format
315
335
  main_type, subtype = type.split(":", 2)
316
336
 
317
337
  # Validate main type
@@ -326,8 +346,14 @@ module Expressir
326
346
  exit_with_error "Invalid TYPE subtype: #{subtype}. " \
327
347
  "Valid TYPE subtypes are: #{Expressir::Coverage::TYPE_SUBTYPES.join(', ')}"
328
348
  end
349
+ # For FUNCTION, validate subtype
350
+ elsif main_type == "FUNCTION"
351
+ unless subtype == "INNER"
352
+ exit_with_error "Invalid FUNCTION subtype: #{subtype}. " \
353
+ "Valid FUNCTION subtypes are: INNER"
354
+ end
329
355
  else
330
- exit_with_error "Subtype syntax (#{type}) is only supported for TYPE entities"
356
+ exit_with_error "Subtype syntax (#{type}) is only supported for TYPE and FUNCTION entities"
331
357
  end
332
358
  else
333
359
  # Handle simple type format
@@ -337,6 +363,55 @@ module Expressir
337
363
  end
338
364
  end
339
365
  end
366
+
367
+ # Parse and expand ignore files from YAML
368
+ # @return [Hash] Hash mapping absolute file paths to their matched patterns
369
+ def parse_ignore_files
370
+ ignore_files_option = options["ignore_files"] || options[:ignore_files]
371
+ return {} unless ignore_files_option
372
+
373
+ unless File.exist?(ignore_files_option)
374
+ say "Warning: Ignore files YAML not found: #{ignore_files_option}"
375
+ return {}
376
+ end
377
+
378
+ begin
379
+ patterns = YAML.load_file(ignore_files_option)
380
+ unless patterns.is_a?(Array)
381
+ say "Warning: Invalid ignore files YAML format. Expected an array of file patterns."
382
+ return {}
383
+ end
384
+
385
+ ignore_files_dir = File.dirname(File.expand_path(ignore_files_option))
386
+ expanded_files = {}
387
+
388
+ patterns.each do |pattern|
389
+ # Resolve pattern relative to the YAML file's directory
390
+ full_pattern = File.expand_path(pattern, ignore_files_dir)
391
+
392
+ # Expand glob pattern
393
+ matched_files = Dir.glob(full_pattern)
394
+
395
+ if matched_files.empty?
396
+ say "Warning: No files matched pattern: #{pattern}"
397
+ else
398
+ matched_files.each do |file_path|
399
+ # Store absolute path and the original pattern that matched it
400
+ expanded_files[File.expand_path(file_path)] = pattern
401
+ end
402
+ end
403
+ end
404
+
405
+ if expanded_files.any?
406
+ say "Found #{expanded_files.size} files to ignore from patterns"
407
+ end
408
+
409
+ expanded_files
410
+ rescue StandardError => e
411
+ say "Warning: Error processing ignore files YAML #{ignore_files_option}: #{e.message}"
412
+ {}
413
+ end
414
+ end
340
415
  end
341
416
  end
342
417
  end
@@ -59,14 +59,16 @@ module Expressir
59
59
  # Represents a documentation coverage report for EXPRESS schemas
60
60
  class Report
61
61
  attr_reader :repository, :schema_reports, :total_entities, :documented_entities,
62
- :undocumented_entities
62
+ :undocumented_entities, :ignored_files
63
63
 
64
64
  # Initialize a coverage report
65
65
  # @param repository [Expressir::Model::Repository] The repository to analyze
66
66
  # @param skip_types [Array<String>] Array of entity type names to skip from coverage
67
- def initialize(repository, skip_types = [])
67
+ # @param ignored_files [Hash] Hash mapping absolute file paths to their matched patterns
68
+ def initialize(repository, skip_types = [], ignored_files = {})
68
69
  @repository = repository
69
70
  @skip_types = skip_types
71
+ @ignored_files = ignored_files
70
72
  @schema_reports = []
71
73
  @total_entities = []
72
74
  @documented_entities = []
@@ -78,18 +80,20 @@ module Expressir
78
80
  # Create a report from a repository
79
81
  # @param repository [Expressir::Model::Repository] The repository to analyze
80
82
  # @param skip_types [Array<String>] Array of entity type names to skip from coverage
83
+ # @param ignored_files [Hash] Hash mapping absolute file paths to their matched patterns
81
84
  # @return [Report] The coverage report
82
- def self.from_repository(repository, skip_types = [])
83
- new(repository, skip_types)
85
+ def self.from_repository(repository, skip_types = [], ignored_files = {})
86
+ new(repository, skip_types, ignored_files)
84
87
  end
85
88
 
86
89
  # Create a report from a schema file
87
90
  # @param path [String] Path to the schema file
88
91
  # @param skip_types [Array<String>] Array of entity type names to skip from coverage
92
+ # @param ignored_files [Hash] Hash mapping absolute file paths to their matched patterns
89
93
  # @return [Report] The coverage report
90
- def self.from_file(path, skip_types = [])
94
+ def self.from_file(path, skip_types = [], ignored_files = {})
91
95
  repository = Expressir::Express::Parser.from_file(path)
92
- new(repository, skip_types)
96
+ new(repository, skip_types, ignored_files)
93
97
  end
94
98
 
95
99
  # Calculate the overall coverage percentage
@@ -113,6 +117,39 @@ module Expressir
113
117
  absolute_path
114
118
  end
115
119
 
120
+ file_report = {
121
+ "file" => relative_path,
122
+ "file_basename" => File.basename(absolute_path),
123
+ "directory" => File.dirname(absolute_path),
124
+ "total" => report[:total].size,
125
+ "documented" => report[:documented].size,
126
+ "undocumented" => report[:undocumented],
127
+ "coverage" => report[:coverage],
128
+ "ignored" => report[:ignored] || false,
129
+ }
130
+
131
+ # Add matched pattern for ignored files
132
+ if report[:ignored] && report[:matched_pattern]
133
+ file_report["matched_pattern"] = report[:matched_pattern]
134
+ end
135
+
136
+ file_report
137
+ end
138
+ end
139
+
140
+ # Get ignored file reports
141
+ # @return [Array<Hash>] Array of ignored file report hashes
142
+ def ignored_file_reports
143
+ @schema_reports.select { |report| report[:ignored] }.map do |report|
144
+ absolute_path = report[:schema].file
145
+ relative_path = begin
146
+ Pathname.new(absolute_path).relative_path_from(Pathname.pwd).to_s
147
+ rescue ArgumentError
148
+ # If paths are on different drives or otherwise incompatible,
149
+ # fall back to the absolute path
150
+ absolute_path
151
+ end
152
+
116
153
  {
117
154
  "file" => relative_path,
118
155
  "file_basename" => File.basename(absolute_path),
@@ -121,6 +158,7 @@ module Expressir
121
158
  "documented" => report[:documented].size,
122
159
  "undocumented" => report[:undocumented],
123
160
  "coverage" => report[:coverage],
161
+ "matched_pattern" => report[:matched_pattern],
124
162
  }
125
163
  end
126
164
  end
@@ -182,11 +220,14 @@ module Expressir
182
220
  schema_report = process_schema(schema)
183
221
  @schema_reports << schema_report
184
222
 
185
- @total_entities.concat(schema_report[:total])
186
- @documented_entities.concat(schema_report[:documented])
187
- @undocumented_entities.concat(schema_report[:undocumented].map do |entity|
188
- { schema: schema.id, entity: entity }
189
- end)
223
+ # Only include non-ignored files in overall statistics
224
+ unless schema_report[:ignored]
225
+ @total_entities.concat(schema_report[:total])
226
+ @documented_entities.concat(schema_report[:documented])
227
+ @undocumented_entities.concat(schema_report[:undocumented].map do |entity|
228
+ { schema: schema.id, entity: entity }
229
+ end)
230
+ end
190
231
  end
191
232
  end
192
233
 
@@ -200,12 +241,19 @@ module Expressir
200
241
 
201
242
  coverage = entities.empty? ? 100.0 : (documented.size.to_f / entities.size) * 100
202
243
 
244
+ # Check if this schema file is ignored
245
+ schema_file = File.expand_path(schema.file) if schema.file
246
+ ignored = @ignored_files.key?(schema_file)
247
+ matched_pattern = @ignored_files[schema_file] if ignored
248
+
203
249
  {
204
250
  schema: schema,
205
251
  total: entities,
206
252
  documented: documented,
207
253
  undocumented: undocumented.map { |e| format_entity(e) },
208
254
  coverage: coverage,
255
+ ignored: ignored,
256
+ matched_pattern: matched_pattern,
209
257
  }
210
258
  end
211
259
 
@@ -413,16 +461,19 @@ module Expressir
413
461
  def self.filter_skipped_entities(entities, skip_types)
414
462
  return entities if skip_types.empty?
415
463
 
416
- # Parse skip_types into simple types and TYPE subtypes
464
+ # Parse skip_types into simple types, TYPE subtypes, and FUNCTION subtypes
417
465
  simple_skips = []
418
466
  type_subtype_skips = []
467
+ function_subtype_skips = []
419
468
 
420
469
  skip_types.each do |skip_type|
421
470
  if skip_type.include?(":")
422
- # Handle TYPE:SUBTYPE format
471
+ # Handle TYPE:SUBTYPE and FUNCTION:SUBTYPE format
423
472
  main_type, subtype = skip_type.split(":", 2)
424
473
  if main_type == "TYPE" && TYPE_SUBTYPES.include?(subtype)
425
474
  type_subtype_skips << subtype
475
+ elsif main_type == "FUNCTION" && subtype == "INNER"
476
+ function_subtype_skips << subtype
426
477
  end
427
478
  else
428
479
  # Handle simple type format
@@ -445,6 +496,9 @@ module Expressir
445
496
  elsif entity_class == "Expressir::Model::Declarations::Type" && type_subtype_skips.any?
446
497
  entity_subtype = get_type_subtype(entity)
447
498
  type_subtype_skips.include?(entity_subtype)
499
+ # Check FUNCTION:INNER exclusions
500
+ elsif entity_class == "Expressir::Model::Declarations::Function" && function_subtype_skips.include?("INNER")
501
+ is_inner_function?(entity)
448
502
  else
449
503
  false
450
504
  end
@@ -470,5 +524,18 @@ module Expressir
470
524
  underlying_class.split("::").last&.upcase
471
525
  end
472
526
  end
527
+
528
+ # Check if a function is an inner function (nested within another function, rule, or procedure)
529
+ # @param function_entity [Expressir::Model::Declarations::Function] The function entity to check
530
+ # @return [Boolean] True if the function is nested within another function, rule, or procedure
531
+ def self.is_inner_function?(function_entity)
532
+ return false unless function_entity.respond_to?(:parent) && function_entity.parent
533
+
534
+ # Check if the parent is a function, rule, or procedure (not a schema)
535
+ parent = function_entity.parent
536
+ parent.is_a?(Expressir::Model::Declarations::Function) ||
537
+ parent.is_a?(Expressir::Model::Declarations::Rule) ||
538
+ parent.is_a?(Expressir::Model::Declarations::Procedure)
539
+ end
473
540
  end
474
541
  end
@@ -208,7 +208,8 @@ module Expressir
208
208
  private
209
209
 
210
210
  def format_repository(node)
211
- node.schemas&.map { |x| format(x) }&.join("\n\n")
211
+ result = node.schemas&.map { |x| format(x) }&.join("\n\n")
212
+ result ? "#{result}\n" : ""
212
213
  end
213
214
 
214
215
  def format_declarations_attribute(node)
@@ -1615,39 +1616,65 @@ module Expressir
1615
1616
  end
1616
1617
 
1617
1618
  def format_remark(node, remark)
1619
+ # Handle embedded remarks
1618
1620
  if remark.include?("\n")
1619
1621
  [
1620
1622
  [
1621
- "(*",
1622
- '"',
1623
+ "(*\"",
1623
1624
  node.path || node.id,
1624
- '"',
1625
+ "\"",
1625
1626
  ].join,
1626
1627
  remark,
1627
1628
  "*)",
1628
1629
  ].join("\n")
1629
1630
  else
1631
+ # Handle tail remarks
1630
1632
  [
1631
- "--",
1632
- '"',
1633
+ "--\"",
1633
1634
  node.path || node.id,
1634
- '"',
1635
- " ",
1635
+ "\" ",
1636
+ remark,
1637
+ ].join
1638
+ end
1639
+ end
1640
+
1641
+ def format_untagged_remark(remark)
1642
+ # Handle embedded remarks
1643
+ if remark.include?("\n")
1644
+ [
1645
+ "(*",
1646
+ remark,
1647
+ "*)",
1648
+ ].join("\n")
1649
+ else
1650
+ # Handle tail remarks
1651
+ [
1652
+ "-- ",
1636
1653
  remark,
1637
1654
  ].join
1638
1655
  end
1639
1656
  end
1640
1657
 
1641
1658
  def format_remarks(node)
1659
+ remarks = []
1660
+
1661
+ # Add tagged remarks
1642
1662
  if node.class.method_defined?(:remarks) && !@no_remarks &&
1643
1663
  !node.remarks.nil?
1644
-
1645
- node.remarks.map do |remark|
1664
+ remarks.concat(node.remarks.map do |remark|
1646
1665
  format_remark(node, remark)
1647
- end
1648
- else
1649
- []
1666
+ end)
1667
+ end
1668
+
1669
+ # Add untagged remarks
1670
+ if node.respond_to?(:untagged_remarks) && !@no_remarks &&
1671
+ !node.untagged_remarks.nil?
1672
+ remarks.concat(node.untagged_remarks.map do |remark|
1673
+ format_untagged_remark(remark)
1674
+ end)
1650
1675
  end
1676
+
1677
+ remarks
1651
1678
  end
1652
1679
 
1653
1680
  def format_scope_remarks(node)
@@ -271,32 +271,67 @@ module Expressir
271
271
  remark_tokens = remark_tokens.reject { |x| @attached_remark_tokens.include?(x) }
272
272
 
273
273
  # parse remarks, find remark targets
274
- tagged_remark_tokens = remark_tokens.map do |span|
274
+ tagged_remark_tokens = []
275
+ untagged_remark_tokens = []
276
+
277
+ remark_tokens.each do |span|
275
278
  text = @source[span[0]..span[1] - 1]
276
- _, remark_tag, remark_text = if text.start_with?("--")
277
- text.match(/^--"([^"]*)"(.*)$/).to_a
278
- else
279
- text.match(/^\(\*"([^"]*)"(.*)\*\)$/m).to_a
280
- end
281
-
282
- if remark_tag
283
- remark_target = find_remark_target(node, remark_tag)
284
- end
285
- if remark_text
286
- remark_text = remark_text.strip.force_encoding("UTF-8")
279
+ remark_type = if text.start_with?("--")
280
+ :tail
281
+ else
282
+ :embedded
283
+ end
284
+
285
+ if text.start_with?("--\"") && text.include?("\"")
286
+ # Tagged tail remark: --"tag" content
287
+ quote_end = text.index("\"", 3)
288
+ if quote_end
289
+ remark_target_path = text[3...quote_end]
290
+ remark_text = text[(quote_end + 1)..].strip.force_encoding("UTF-8")
291
+ remark_target = find_remark_target(node, remark_target_path)
292
+ if remark_target
293
+ tagged_remark_tokens << [span, remark_target, remark_text]
294
+ end
295
+ end
296
+ elsif text.start_with?("(*\"") && text.include?("\"")
297
+ # Tagged embedded remark: (*"tag" content *)
298
+ quote_end = text.index("\"", 3)
299
+ if quote_end
300
+ remark_target_path = text[3...quote_end]
301
+ remark_text = text[(quote_end + 1)...-2].strip.force_encoding("UTF-8")
302
+ remark_target = find_remark_target(node, remark_target_path)
303
+ if remark_target
304
+ tagged_remark_tokens << [span, remark_target, remark_text]
305
+ end
306
+ end
307
+ elsif text.start_with?("--")
308
+ # Untagged tail remark: -- content
309
+ untagged_text = text[2..].strip.force_encoding("UTF-8")
310
+ untagged_remark_tokens << [span, untagged_text, remark_type]
311
+ else
312
+ # Untagged embedded remark: (* content *)
313
+ untagged_text = text[2...-2].strip.force_encoding("UTF-8")
314
+ untagged_remark_tokens << [span, untagged_text, remark_type]
287
315
  end
316
+ end
288
317
 
289
- [span, remark_target, remark_text]
290
- end.select { |x| x[1] }
291
-
318
+ # Attach tagged remarks
292
319
  tagged_remark_tokens.each do |span, remark_target, remark_text|
293
- # attach remark
294
320
  remark_target.remarks ||= []
295
321
  remark_target.remarks << remark_text
296
-
297
- # mark remark as attached, so that it is not attached again at higher nesting level
298
322
  @attached_remark_tokens << span
299
323
  end
324
+
325
+ # Attach untagged remarks to the current node if it supports them
326
+ # All ModelElements support untagged remarks, but we may get Arrays here
327
+ if node.respond_to?(:untagged_remarks) && !untagged_remark_tokens.empty?
328
+ node.untagged_remarks ||= []
329
+ untagged_remark_tokens.each do |span, untagged_text, _remark_type|
330
+ # Handle both embedded and tail remarks
331
+ node.untagged_remarks << untagged_text
332
+ @attached_remark_tokens << span
333
+ end
334
+ end
300
335
  end
301
336
 
302
337
  def visit_attribute_ref(ctx)
@@ -5,11 +5,13 @@ module Expressir
5
5
  mod.attribute :id, :string
6
6
  mod.attribute :remarks, :string, collection: true
7
7
  mod.attribute :remark_items, ::Expressir::Model::Declarations::RemarkItem, collection: true
8
+ mod.attribute :untagged_remarks, :string, collection: true
8
9
 
9
10
  mod.key_value do
10
11
  map "id", to: :id
11
12
  map "remarks", to: :remarks
12
13
  map "remark_items", to: :remark_items
14
+ map "untagged_remarks", to: :untagged_remarks
13
15
  end
14
16
  end
15
17
  end
@@ -13,6 +13,7 @@ module Expressir
13
13
  attribute :_class, :string, default: -> { send(:name) },
14
14
  polymorphic_class: true
15
15
  attribute :source, :string
16
+ attribute :untagged_remarks, :string, collection: true
16
17
 
17
18
  # TODO: Add basic mappings that can be inherited by all subclasses
18
19
  key_value do
@@ -1,3 +1,3 @@
1
1
  module Expressir
2
- VERSION = "2.1.20".freeze
2
+ VERSION = "2.1.22".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: expressir
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.20
4
+ version: 2.1.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-06-05 00:00:00.000000000 Z
11
+ date: 2025-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64