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 +4 -4
- data/.rubocop_todo.yml +51 -13
- data/README.adoc +196 -0
- data/lib/expressir/cli.rb +1 -0
- data/lib/expressir/commands/coverage.rb +100 -25
- data/lib/expressir/coverage.rb +80 -13
- data/lib/expressir/express/formatter.rb +40 -13
- data/lib/expressir/express/visitor.rb +53 -18
- data/lib/expressir/model/identifier.rb +2 -0
- data/lib/expressir/model/model_element.rb +1 -0
- data/lib/expressir/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fab262d4a8c290f1dc292ff8ea22e9f631356945fb76bed736594dad060078b0
|
4
|
+
data.tar.gz: 45e33a9920262038833f1f00113b12527eb9dc64a71d29d8e0b5f36bd89842ff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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-
|
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:
|
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:
|
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:
|
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:
|
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:
|
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
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
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
|
data/lib/expressir/coverage.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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
|
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
|
-
|
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 =
|
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
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
if
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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
|
-
|
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
|
data/lib/expressir/version.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2025-06-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: base64
|