Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -248,14 +248,14 @@ use_repo(pip, "manual_analysis_deps")
bazel_dep(name = "trlc", version = "0.0.0")
git_override(
module_name = "trlc",
commit = "8bdcb1fe6d5af97b69a293673815592371bdbb20",
commit = "08ff8c7d6c362153392003c671a9799a446b0db5",
remote = "https://github.com/bmw-software-engineering/trlc.git",
)

bazel_dep(name = "lobster", version = "0.0.0")
git_override(
module_name = "lobster",
commit = "d528fbdec2cd72ff7967b51546fb0bd935810258",
commit = "9da4a491ef65c33f50e82d1e29346180dd5acc60",
remote = "https://github.com/bmw-software-engineering/lobster.git",
)

Expand Down
7 changes: 6 additions & 1 deletion bazel/rules/rules_score/private/dependable_element.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,11 @@ def _process_artifact_files(ctx, artifact_name, label):
# Compute paths
relative_path = _compute_relative_path(artifact_file, common_dir)

# Document files (rst/md) that are transitive deps but not owned by
# this rule are already placed in their own artifact section.
if _is_document_file(artifact_file) and artifact_file.path not in srcs_paths:
continue

# Create symlink
output_file = _create_artifact_symlink(
ctx,
Expand All @@ -278,7 +283,7 @@ def _process_artifact_files(ctx, artifact_name, label):
output_files.append(output_file)

# Add to toctree index only for files directly owned by this rule.
if _is_document_file(artifact_file) and artifact_file.path in srcs_paths:
if _is_document_file(artifact_file):
doc_ref = (artifact_name + "/" + relative_path) \
.replace(".rst", "") \
.replace(".md", "")
Expand Down
28 changes: 6 additions & 22 deletions plantuml/parser/puml_serializer/src/fbs/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,8 @@ rust_library(
srcs = [
":component_fbs_codegen",
],
# Generated code from flatc - suppress clippy warnings we can't fix
rustc_flags = [
"--allow=unused_imports",
"--allow=clippy::extra-unused-lifetimes",
"--allow=clippy::missing-safety-doc",
"--allow=clippy::needless-lifetimes",
"--allow=mismatched_lifetime_syntaxes",
],
# Generated code from flatc - suppress all lints we can't fix
rustc_flags = ["--cap-lints=allow"],
visibility = [
"//visibility:public",
],
Expand All @@ -65,13 +59,8 @@ rust_library(
srcs = [
":class_fbs_codegen",
],
rustc_flags = [
"--allow=unused_imports",
"--allow=clippy::extra-unused-lifetimes",
"--allow=clippy::missing-safety-doc",
"--allow=clippy::needless-lifetimes",
"--allow=mismatched_lifetime_syntaxes",
],
# Generated code from flatc - suppress all lints we can't fix
rustc_flags = ["--cap-lints=allow"],
visibility = [
"//plantuml/parser:__subpackages__",
"//validation/core:__subpackages__",
Expand Down Expand Up @@ -99,13 +88,8 @@ rust_library(
srcs = [
":sequence_fbs_codegen",
],
rustc_flags = [
"--allow=unused_imports",
"--allow=clippy::extra-unused-lifetimes",
"--allow=clippy::missing-safety-doc",
"--allow=clippy::needless-lifetimes",
"--allow=mismatched_lifetime_syntaxes",
],
# Generated code from flatc - suppress all lints we can't fix
rustc_flags = ["--cap-lints=allow"],
visibility = [
"//plantuml/parser:__subpackages__",
"//validation/core:__subpackages__",
Expand Down
16 changes: 14 additions & 2 deletions validation/ai_checker/ai_checker.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ load("//bazel/rules/rules_score:providers.bzl", "ArchitecturalDesignInfo")
# Shared implementation
# ============================================================================

def _run_ai_analysis(ctx, analysis_files, all_input_files, input_dirs, dep_dirs):
def _run_ai_analysis(ctx, analysis_files, all_input_files, input_dirs, dep_dirs, req_files = None):
"""Common implementation for all AI artefact analysis test rules.

Args:
Expand All @@ -32,6 +32,10 @@ def _run_ai_analysis(ctx, analysis_files, all_input_files, input_dirs, dep_dirs)
all_input_files: All files needed as action inputs (incl. deps for resolution).
input_dirs: Dict of directories containing analysis files.
dep_dirs: Dict of dependency directories (for link resolution).
req_files: Optional list of individual files to register with TRLC instead
of scanning the entire input directory. When set, only these files are
parsed; other files present in the same directory are ignored. This
avoids picking up unreferenced files that may fail TRLC validation.

Returns:
List of providers (DefaultInfo).
Expand Down Expand Up @@ -63,6 +67,14 @@ def _run_ai_analysis(ctx, analysis_files, all_input_files, input_dirs, dep_dirs)
if extra_dir != input_dir:
args.add("--deps", extra_dir)

# When individual req files are provided, pass them explicitly so the
# extractor registers only those files and ignores other files present in
# the same directory (e.g. files not declared in Bazel srcs that may fail
# TRLC validation).
if req_files:
for f in req_files:
args.add("--req-file", f)

args.add("--output", json_report.path)
args.add("--html", html_report.path)
args.add("--guidelines-output", guidelines_output_dir.path)
Expand Down Expand Up @@ -212,7 +224,7 @@ def _trlc_requirements_ai_test_impl(ctx):
for f in dep_reqs + spec_files:
dep_dirs[f.dirname] = True

return _run_ai_analysis(ctx, analysis_files, all_files, input_dirs, dep_dirs)
return _run_ai_analysis(ctx, analysis_files, all_files, input_dirs, dep_dirs, req_files = analysis_files)

trlc_requirements_ai_test = rule(
implementation = _trlc_requirements_ai_test_impl,
Expand Down
33 changes: 30 additions & 3 deletions validation/ai_checker/src/ai_checker/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,10 @@ def __init__(
self._extracted_artefacts = None

def analyze_directory(
self, input_dir: str, dependency_dirs: list[str] | None = None
self,
input_dir: str,
dependency_dirs: list[str] | None = None,
req_files: list[str] | None = None,
) -> AnalysisResults:
"""
Extract and analyze artefacts from a directory using TRLC
Expand All @@ -176,12 +179,20 @@ def analyze_directory(
input_dir: Path to directory containing files to analyze
dependency_dirs: Optional list of directories containing
dependencies for link resolution
req_files: Optional list of individual TRLC files to register
instead of scanning the entire input directory. When set,
only these files are parsed so that unreferenced files
present in the same directory are not picked up.

Returns:
AnalysisResults containing structured analyses for each artefact
"""
# Initialize TRLC requirement extractor
self.artefact_extractor = RequirementExtractor(input_dir, dependency_dirs)
self.artefact_extractor = RequirementExtractor(
input_dir,
dependency_dirs,
req_files=req_files or [],
)

# Extract artefacts
artefacts = self.artefact_extractor.extract()
Expand Down Expand Up @@ -256,6 +267,18 @@ def argument_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Analyze TRLC requirements against engineering guidelines"
)
parser.add_argument(
"--req-file",
action="append",
default=[],
dest="req_file",
help=(
"Individual TRLC file to register for analysis "
"(can be specified multiple times). When provided, only these "
"files are registered from the input directory instead of "
"scanning the entire directory."
),
)
parser.add_argument(
"-i",
"--input",
Expand Down Expand Up @@ -362,7 +385,11 @@ def main() -> None:
max_concurrent_requests=args.max_concurrent_requests,
max_batch_chars=args.max_batch_chars,
)
analysis_results = orchestrator.analyze_directory(args.input, args.deps)
analysis_results = orchestrator.analyze_directory(
args.input,
args.deps,
req_files=args.req_file or None,
)

# Format and output results
orchestrator.format_and_output(
Expand Down
93 changes: 60 additions & 33 deletions validation/ai_checker/src/ai_checker/requirement_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ class RequirementExtractor(ArtefactExtractor):
"""Extracts structured requirement data from TRLC files."""

def __init__(
self, input_directory: str, dependency_directories: list[str] | None = None
self,
input_directory: str,
dependency_directories: list[str] | None = None,
req_files: list[str] | None = None,
):
"""
Initialize the RequirementExtractor with directory paths.
Expand All @@ -42,18 +45,28 @@ def __init__(
analyze
dependency_directories: Optional list of additional
directories for link resolution
req_files: Optional list of individual TRLC files to register
instead of scanning the entire input directory. When set,
only these files are registered so that other files present
in the same directory (e.g. files not declared in Bazel
srcs) are not picked up by TRLC.
"""
self.input_directory = os.path.abspath(input_directory)
self.dependency_directories = [
os.path.abspath(d) for d in (dependency_directories or [])
]
self.req_files = [os.path.abspath(f) for f in (req_files or [])]
self.symbols: trlc.ast.Symbol_Table | None = None

def parse_trlc_files(self) -> trlc.ast.Symbol_Table:
"""
Parse TRLC files in the specified directories.

Registers all directories (input + dependencies) with TRLC for link resolution.
When ``req_files`` was supplied at construction time, only those
individual files are registered from the input directory; dependency
directories are still registered in full for link resolution.
When ``req_files`` is empty (the default), all directories including
the input directory are registered (original behaviour).

Returns:
Symbol table containing parsed TRLC objects
Expand All @@ -64,37 +77,51 @@ def parse_trlc_files(self) -> trlc.ast.Symbol_Table:
message_handler = Message_Handler()
source_manager = Source_Manager(message_handler)

# Collect all directories and filter out overlapping ones
all_dirs = [self.input_directory] + self.dependency_directories

# Remove duplicates and filter out directories that are
# subdirectories of others
unique_dirs = []
for dir_path in sorted(set(all_dirs)):
# Check if this directory is a subdirectory of any already
# registered directory
is_subdir = False
for existing_dir in unique_dirs:
if dir_path.startswith(existing_dir + os.sep):
is_subdir = True
break

# Also check if any existing directory is a subdirectory of this one
# In that case, remove the existing one and add this one
dirs_to_remove = []
for i, existing_dir in enumerate(unique_dirs):
if existing_dir.startswith(dir_path + os.sep):
dirs_to_remove.append(i)

for i in reversed(dirs_to_remove):
unique_dirs.pop(i)

if not is_subdir:
unique_dirs.append(dir_path)

# Register all unique, non-overlapping directories
for dir_path in unique_dirs:
source_manager.register_directory(dir_path)
if self.req_files:
# Register only the specific req files declared in the Bazel target.
# This avoids picking up extra .trlc files in the same directory that
# are not part of the target and may fail TRLC validation.
for file_path in self.req_files:
source_manager.register_file(file_path)

# Register dependency directories in full for cross-reference / link
# resolution (these dirs are controlled by Bazel deps and are expected
# to be valid).
for dep_dir in self.dependency_directories:
source_manager.register_directory(dep_dir)
else:
# Original behaviour: register all directories (input + deps).
# Collect all directories and filter out overlapping ones.
all_dirs = [self.input_directory] + self.dependency_directories

# Remove duplicates and filter out directories that are
# subdirectories of others
unique_dirs = []
for dir_path in sorted(set(all_dirs)):
# Check if this directory is a subdirectory of any already
# registered directory
is_subdir = False
for existing_dir in unique_dirs:
if dir_path.startswith(existing_dir + os.sep):
is_subdir = True
break

# Also check if any existing directory is a subdirectory of this one
# In that case, remove the existing one and add this one
dirs_to_remove = []
for i, existing_dir in enumerate(unique_dirs):
if existing_dir.startswith(dir_path + os.sep):
dirs_to_remove.append(i)

for i in reversed(dirs_to_remove):
unique_dirs.pop(i)

if not is_subdir:
unique_dirs.append(dir_path)

# Register all unique, non-overlapping directories
for dir_path in unique_dirs:
source_manager.register_directory(dir_path)

symbols = source_manager.process()
if symbols is None:
Expand Down
Loading
Loading