sourcecode 2.5.3__py3-none-any.whl → 2.5.5__py3-none-any.whl
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.
- sourcecode/__init__.py +1 -1
- sourcecode/architecture_analyzer.py +136 -23
- sourcecode/classifier.py +4 -2
- sourcecode/detectors/java.py +27 -10
- sourcecode/reconciliation.py +346 -0
- sourcecode/repository_ir.py +255 -39
- sourcecode/spring_impact.py +157 -17
- sourcecode/summarizer.py +56 -15
- {sourcecode-2.5.3.dist-info → sourcecode-2.5.5.dist-info}/METADATA +1 -1
- {sourcecode-2.5.3.dist-info → sourcecode-2.5.5.dist-info}/RECORD +13 -12
- {sourcecode-2.5.3.dist-info → sourcecode-2.5.5.dist-info}/WHEEL +0 -0
- {sourcecode-2.5.3.dist-info → sourcecode-2.5.5.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.5.3.dist-info → sourcecode-2.5.5.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -4,6 +4,8 @@ import re
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Literal, Optional
|
|
6
6
|
|
|
7
|
+
from sourcecode.reconciliation import incoherent_layers
|
|
8
|
+
|
|
7
9
|
from sourcecode.schema import (
|
|
8
10
|
ArchitectureAnalysis,
|
|
9
11
|
ArchitectureDomain,
|
|
@@ -31,7 +33,14 @@ _TOOLING_PREFIXES = (
|
|
|
31
33
|
"dist/",
|
|
32
34
|
"build/",
|
|
33
35
|
)
|
|
34
|
-
|
|
36
|
+
# Structural wrappers to walk THROUGH when looking for a module: they are build
|
|
37
|
+
# layout, never a module name. "main" and the language dirs come from the Maven /
|
|
38
|
+
# Gradle source-set convention (src/main/java/...) — without them a Maven repo with
|
|
39
|
+
# no other signal offers "main" as one of its modules (P1-D, spring-petclinic).
|
|
40
|
+
_SRC_TRANSPARENT = {
|
|
41
|
+
"src", "lib", "app", "pkg",
|
|
42
|
+
"main", "java", "kotlin", "scala", "groovy",
|
|
43
|
+
}
|
|
35
44
|
_CODE_EXTENSIONS = {
|
|
36
45
|
".py", ".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs",
|
|
37
46
|
".go", ".java", ".kt", ".rs", ".rb",
|
|
@@ -56,7 +65,13 @@ _BENCHMARK_DIRS: frozenset[str] = frozenset({
|
|
|
56
65
|
"sandbox",
|
|
57
66
|
})
|
|
58
67
|
_DOCS_DIRS: frozenset[str] = frozenset({"docs", "doc", "documentation", "wiki"})
|
|
59
|
-
|
|
68
|
+
# Tooling/CI config, never runtime architecture. The dot-dirs are conventional: a
|
|
69
|
+
# leading dot marks tooling config across ecosystems, and without them `.github`
|
|
70
|
+
# was offered as a neo4j "module" alongside community/ and packaging/ (P1-D).
|
|
71
|
+
_TOOLING_DIRS: frozenset[str] = frozenset({
|
|
72
|
+
"scripts", "script", "tools", "tool", "ci",
|
|
73
|
+
".github", ".circleci", ".mvn", ".idea", ".vscode",
|
|
74
|
+
})
|
|
60
75
|
# All dirs that are not part of the runtime source architecture
|
|
61
76
|
_NON_SOURCE_DIRS: frozenset[str] = _TEST_DIRS | _BENCHMARK_DIRS | _DOCS_DIRS | _TOOLING_DIRS | _ASSET_DIRS
|
|
62
77
|
|
|
@@ -126,8 +141,14 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
|
|
|
126
141
|
"application": ["application", "usecases", "usecase"],
|
|
127
142
|
"infrastructure": ["infrastructure", "infra", "adapters", "persistence"],
|
|
128
143
|
},
|
|
144
|
+
# P1-D residue (found by measurement, 2026-07-16): "core" belongs with
|
|
145
|
+
# "api"/"store"/"db" — it names a module's importance ("the main module"),
|
|
146
|
+
# never an onion/hexagonal DOMAIN layer, and nearly every large JVM repo has
|
|
147
|
+
# one. With it, keycloak's `core/` (623 Java files) + a
|
|
148
|
+
# `representations/adapters/config` package read "Onion Architecture with
|
|
149
|
+
# domain, adapters layers". Kept are the names that state their role.
|
|
129
150
|
"onion": {
|
|
130
|
-
"domain": ["domain"
|
|
151
|
+
"domain": ["domain"],
|
|
131
152
|
"application": ["application", "usecases"],
|
|
132
153
|
"ports": ["ports", "interfaces"],
|
|
133
154
|
"adapters": ["adapters", "secondary"],
|
|
@@ -137,16 +158,30 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
|
|
|
137
158
|
"model": ["model", "models", "entity", "entities", "domain"],
|
|
138
159
|
"view": ["views", "templates", "pages", "components"],
|
|
139
160
|
},
|
|
161
|
+
# P1-D: a keyword may only name a layer it actually ATTESTS. The dropped
|
|
162
|
+
# synonyms below are naming conventions that say nothing about a layer role,
|
|
163
|
+
# and they were the engine of the fabricated claims — mass alone cannot filter
|
|
164
|
+
# them out, because in the repos they mislabel they are large:
|
|
165
|
+
# * "api" — a Java `foo/api/` package is a public API surface (interfaces),
|
|
166
|
+
# not an HTTP controller layer. It manufactured neo4j's controller layer
|
|
167
|
+
# (925 files, 16.5%, and neo4j has no controller dir at all) and openmrs's
|
|
168
|
+
# (821 files, 94.8% — that is `org/openmrs/api/`, the service API).
|
|
169
|
+
# * "store"/"storage" — a storage ENGINE is not a repository/DAO layer
|
|
170
|
+
# (neo4j's `store` is exactly that).
|
|
171
|
+
# * "db"/"database" — a database package is not an infrastructure layer.
|
|
172
|
+
# Kept are the names that state their role: controller/handlers/routes/endpoints,
|
|
173
|
+
# repository/repo/dao, infra/infrastructure/persistence. Broadleaf keeps all four
|
|
174
|
+
# of its layers on those alone.
|
|
140
175
|
"layered": {
|
|
141
|
-
"controller": ["controller", "controllers", "
|
|
176
|
+
"controller": ["controller", "controllers", "routes", "handlers", "endpoints"],
|
|
142
177
|
"service": ["service", "services", "usecase", "usecases", "application"],
|
|
143
|
-
"repository": ["repository", "repositories", "repo", "repos", "
|
|
144
|
-
"infrastructure": ["infra", "infrastructure", "persistence"
|
|
178
|
+
"repository": ["repository", "repositories", "repo", "repos", "dao"],
|
|
179
|
+
"infrastructure": ["infra", "infrastructure", "persistence"],
|
|
145
180
|
},
|
|
146
181
|
"hexagonal": {
|
|
147
182
|
"port": ["port", "ports", "interface", "interfaces"],
|
|
148
183
|
"adapter": ["adapter", "adapters"],
|
|
149
|
-
"domain": ["domain", "
|
|
184
|
+
"domain": ["domain", "model", "models"], # "core" dropped — see onion
|
|
150
185
|
},
|
|
151
186
|
"monorepo": {
|
|
152
187
|
"apps": ["apps", "applications"],
|
|
@@ -154,7 +189,11 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
|
|
|
154
189
|
},
|
|
155
190
|
"fullstack": {
|
|
156
191
|
"frontend": ["frontend", "client", "web", "ui", "pages", "components", "app"],
|
|
157
|
-
|
|
192
|
+
# P1-D: "api" dropped — a `foo/api/` package is a public API surface, not
|
|
193
|
+
# evidence of a backend TIER. With it, openmrs (94.8% `org/openmrs/api/`)
|
|
194
|
+
# and jobrunr fell straight from a fabricated "layered" into a fabricated
|
|
195
|
+
# "fullstack" instead. Same conflation, same fix.
|
|
196
|
+
"backend": ["backend", "server", "services"],
|
|
158
197
|
},
|
|
159
198
|
}
|
|
160
199
|
|
|
@@ -165,8 +204,37 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
|
|
|
165
204
|
# it; without it the pattern falls through to layered / a weaker match.
|
|
166
205
|
_PATTERN_REQUIRED_KEYS: dict[str, frozenset[str]] = {
|
|
167
206
|
"mvc": frozenset({"view"}),
|
|
207
|
+
# Same principle as MVC/view — a pattern must show its DEFINING trait, not
|
|
208
|
+
# just any two of its layers. Hexagonal is literally "Ports and Adapters":
|
|
209
|
+
# a `models/` package next to an `adapters/` package is not one, and that
|
|
210
|
+
# pair alone was enough to label keycloak "Hexagonal Architecture". Onion's
|
|
211
|
+
# defining trait is the domain at the centre.
|
|
212
|
+
"hexagonal": frozenset({"port", "adapter"}),
|
|
213
|
+
"onion": frozenset({"domain"}),
|
|
168
214
|
}
|
|
169
215
|
|
|
216
|
+
# Minimum share of source files a matched layer must hold to count as a LAYER
|
|
217
|
+
# (P1-D, neo4j field test). Matching was pure name PRESENCE over the set of every
|
|
218
|
+
# directory name in the repo, so one incidental dir minted a whole layer: neo4j — a
|
|
219
|
+
# graph engine with no controller, repository or infrastructure directory anywhere —
|
|
220
|
+
# reported "Layered Architecture with controller, service, repository, infrastructure
|
|
221
|
+
# layers", synthesized from `api`, `service` (7 files), `store`/`storage` and
|
|
222
|
+
# `database`. Presence does not scale: the bigger the repo, the likelier some
|
|
223
|
+
# directory somewhere matches a keyword, so large systems were the most confidently
|
|
224
|
+
# mislabelled.
|
|
225
|
+
#
|
|
226
|
+
# A layer is a place where a real share of the code lives. Same reasoning the
|
|
227
|
+
# archetype engine already applies to the same question ("a 0.3% `api/` dir does not
|
|
228
|
+
# manufacture a controller layer", _architectural_style).
|
|
229
|
+
#
|
|
230
|
+
# Calibrated against the field-test repos rather than copied from archetype's 5%,
|
|
231
|
+
# which scores a web+service+repository triplet and is too coarse to erase a single
|
|
232
|
+
# layer with: Broadleaf's controller layer is a real 4.68% (it has a literal
|
|
233
|
+
# `controller/` dir) and 5% deleted it. The phantoms all sit an order of magnitude
|
|
234
|
+
# lower — jenkins 0.72% (`handlers`, and it has no HTTP controllers at all), ofbiz
|
|
235
|
+
# 0.19%, neo4j's service layer 7 files at 0.12% — so 1% separates them cleanly.
|
|
236
|
+
_LAYER_MIN_SHARE = 0.01
|
|
237
|
+
|
|
170
238
|
# Higher value = wins when score ties
|
|
171
239
|
_PATTERN_PRIORITY: dict[str, int] = {
|
|
172
240
|
"cqrs": 8,
|
|
@@ -555,24 +623,62 @@ class ArchitectureAnalyzer:
|
|
|
555
623
|
if not source_paths:
|
|
556
624
|
return "unknown", []
|
|
557
625
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
626
|
+
# Index each directory NAME to the source files under it, so a matched layer
|
|
627
|
+
# can be weighed by the code it actually holds instead of counted as present
|
|
628
|
+
# (P1-D). Built once; every pattern below reads it.
|
|
629
|
+
dir_files: dict[str, set[int]] = {}
|
|
630
|
+
for i, p in enumerate(source_paths):
|
|
631
|
+
for part in p.replace("\\", "/").split("/")[:-1]:
|
|
632
|
+
dir_files.setdefault(part.lower(), set()).add(i)
|
|
633
|
+
dir_names: set[str] = set(dir_files)
|
|
634
|
+
total_files = len(source_paths)
|
|
635
|
+
|
|
636
|
+
def _layer_files(matched_dirs: list[str]) -> set[int]:
|
|
637
|
+
files: set[int] = set()
|
|
638
|
+
for d in matched_dirs:
|
|
639
|
+
files |= dir_files.get(d, set())
|
|
640
|
+
return files
|
|
563
641
|
|
|
564
642
|
# 1. Classical keyword-based pattern matching
|
|
565
643
|
best_pattern = ""
|
|
566
644
|
best_score = 0
|
|
567
645
|
best_priority = -1
|
|
568
646
|
best_matched: dict[str, list[str]] = {}
|
|
647
|
+
best_layer_files: dict[str, set[int]] = {}
|
|
569
648
|
|
|
570
649
|
for pattern_name, layer_keys in LAYER_PATTERNS.items():
|
|
571
650
|
matched: dict[str, list[str]] = {}
|
|
651
|
+
layer_files: dict[str, set[int]] = {}
|
|
572
652
|
for layer_key, keywords in layer_keys.items():
|
|
573
653
|
matched_dirs = [d for d in dir_names if d in keywords]
|
|
574
|
-
if matched_dirs:
|
|
575
|
-
|
|
654
|
+
if not matched_dirs:
|
|
655
|
+
continue
|
|
656
|
+
# P1-D: a layer is where a real share of the code lives. An incidental
|
|
657
|
+
# directory that merely matches a keyword is not one — neo4j's 7-file
|
|
658
|
+
# `service/` dir (0.12%) never made it a service layer.
|
|
659
|
+
files = _layer_files(matched_dirs)
|
|
660
|
+
if total_files and len(files) / total_files < _LAYER_MIN_SHARE:
|
|
661
|
+
continue
|
|
662
|
+
matched[layer_key] = matched_dirs
|
|
663
|
+
layer_files[layer_key] = files
|
|
664
|
+
|
|
665
|
+
# SIM-3: a layer must belong to the same PROGRAM as the rest of the
|
|
666
|
+
# claim. Directory names are language-blind, so on a polyglot
|
|
667
|
+
# monorepo one pattern was assembled across two subsystems: keycloak
|
|
668
|
+
# read "MVC with controller, model, view layers" where `view` was a
|
|
669
|
+
# React SPA (87 .tsx, zero Java) sitting beside a 1088-file Java
|
|
670
|
+
# `model`. Mass cannot catch this — the SPA has genuine mass. The
|
|
671
|
+
# reconciliation rule drops layers owned by a different runtime;
|
|
672
|
+
# template/asset-only layers are untouched, so a real Spring-MVC
|
|
673
|
+
# `templates/` view survives.
|
|
674
|
+
for _bad in incoherent_layers(
|
|
675
|
+
pattern_name,
|
|
676
|
+
{k: [source_paths[i] for i in v] for k, v in layer_files.items()},
|
|
677
|
+
source_paths,
|
|
678
|
+
):
|
|
679
|
+
matched.pop(_bad, None)
|
|
680
|
+
layer_files.pop(_bad, None)
|
|
681
|
+
|
|
576
682
|
# A pattern with unmet required keys cannot qualify (e.g. mvc needs a view).
|
|
577
683
|
required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
|
|
578
684
|
if required and not required.issubset(matched.keys()):
|
|
@@ -584,18 +690,13 @@ class ArchitectureAnalyzer:
|
|
|
584
690
|
best_priority = priority
|
|
585
691
|
best_pattern = pattern_name
|
|
586
692
|
best_matched = matched
|
|
693
|
+
best_layer_files = layer_files
|
|
587
694
|
|
|
588
695
|
if best_score >= 2:
|
|
589
696
|
layer_confidence: Literal["high", "medium", "low"] = "medium" if best_score >= 3 else "low"
|
|
590
697
|
layers: list[ArchitectureLayer] = []
|
|
591
698
|
for layer_key, matched_dirs in best_matched.items():
|
|
592
|
-
matched_files = [
|
|
593
|
-
p for p in source_paths
|
|
594
|
-
if any(
|
|
595
|
-
seg.lower() in matched_dirs
|
|
596
|
-
for seg in p.replace("\\", "/").split("/")[:-1]
|
|
597
|
-
)
|
|
598
|
-
]
|
|
699
|
+
matched_files = [source_paths[i] for i in sorted(best_layer_files[layer_key])]
|
|
599
700
|
layers.append(ArchitectureLayer(
|
|
600
701
|
name=layer_key,
|
|
601
702
|
pattern=best_pattern,
|
|
@@ -716,6 +817,14 @@ class ArchitectureAnalyzer:
|
|
|
716
817
|
- orchestration: cli.py, main.py, *_handler.py, *_controller.py
|
|
717
818
|
- processing: *_analyzer.py, *_processor.py, *_parser.py, *_detector.py, *_scanner.py
|
|
718
819
|
- data: schema.py, model.py, *_repository.py, *_serializer.py, config.py, …
|
|
820
|
+
|
|
821
|
+
P1-D: weighed by share, like the directory-name patterns. Matching on file
|
|
822
|
+
stems alone let a handful of incidentally-named files mint logical layers:
|
|
823
|
+
neo4j's "orchestration, processing, data" rested on ~6 files out of 5616
|
|
824
|
+
(0.05% each) — one `Main.java`, a `Config.java`, a `Serializer.java`, and two
|
|
825
|
+
`Server.java` that are model classes under `topology/model/`, "orchestration"
|
|
826
|
+
only because of their stem. Three layers named off 0.1% of a codebase is a
|
|
827
|
+
description of nothing.
|
|
719
828
|
"""
|
|
720
829
|
layer_files: dict[str, list[str]] = {"orchestration": [], "processing": [], "data": []}
|
|
721
830
|
for p in paths:
|
|
@@ -728,7 +837,11 @@ class ArchitectureAnalyzer:
|
|
|
728
837
|
layer_files[layer].append(p)
|
|
729
838
|
break
|
|
730
839
|
|
|
731
|
-
|
|
840
|
+
total = len(paths)
|
|
841
|
+
non_empty = {
|
|
842
|
+
k: v for k, v in layer_files.items()
|
|
843
|
+
if v and (not total or len(v) / total >= _LAYER_MIN_SHARE)
|
|
844
|
+
}
|
|
732
845
|
if len(non_empty) >= 2:
|
|
733
846
|
return "layered", [
|
|
734
847
|
ArchitectureLayer(name=k, pattern="layered", files=v, confidence="low")
|
sourcecode/classifier.py
CHANGED
|
@@ -25,7 +25,9 @@ _API_FRAMEWORKS = {
|
|
|
25
25
|
"Micronaut",
|
|
26
26
|
"Vert.x",
|
|
27
27
|
"Quarkus",
|
|
28
|
-
"
|
|
28
|
+
"JAX-RS", # Jakarta REST — pure JAX-RS projects must not fall to "unknown"
|
|
29
|
+
"Jakarta EE", # platform umbrella artifact (jakartaee-api); implies a web profile
|
|
30
|
+
"Java EE", # javax.* umbrella artifact (javaee-api)
|
|
29
31
|
"Laravel",
|
|
30
32
|
"Symfony",
|
|
31
33
|
"Ktor",
|
|
@@ -42,7 +44,7 @@ _API_STACKS = {"python", "go", "java", "php", "ruby", "dotnet", "kotlin", "scala
|
|
|
42
44
|
# guard because they are repo-wide (no locatable minority module):
|
|
43
45
|
# * a jakarta.ws.rs / javax.ws.rs-api coordinate pinned in a BOM/parent pom's
|
|
44
46
|
# <dependencyManagement> (a version pin, not usage), and
|
|
45
|
-
# * a source-annotation "
|
|
47
|
+
# * a source-annotation "JAX-RS" marker synthesized from @Path resources
|
|
46
48
|
# (detected_via carries no file path).
|
|
47
49
|
# So on a repo large enough to HAVE a distinct center of gravity, require the API
|
|
48
50
|
# surface to occupy a real share of the code (or the dominant module) before we
|
sourcecode/detectors/java.py
CHANGED
|
@@ -185,17 +185,22 @@ class JavaDetector(AbstractDetector):
|
|
|
185
185
|
# build.gradle files are not scanned at all — yet it ships 18 @Path resource
|
|
186
186
|
# classes. Without this the headline read "Java library / package" while the
|
|
187
187
|
# endpoints command listed a full JAX-RS API. When the annotation scan found
|
|
188
|
-
# JAX-RS entry points and no
|
|
188
|
+
# JAX-RS entry points and no JAX-RS framework was inferred from the
|
|
189
189
|
# manifest, surface it so the stack (and project_type) reflect the REST API
|
|
190
190
|
# the endpoints already prove. Structural (VAI): reasons from the JAX-RS
|
|
191
191
|
# entry points found in source, not from a vendor dependency-name allowlist.
|
|
192
192
|
# detected_via carries no path form, so it is treated as repo-wide evidence
|
|
193
193
|
# (never localized to an adapter submodule).
|
|
194
|
-
|
|
194
|
+
# P1-C: @Path/@GET resources prove JAX-RS — the one spec they belong to —
|
|
195
|
+
# and nothing about the rest of the Jakarta EE platform. Eureka (Jersey 1.x
|
|
196
|
+
# + Guice, no EE container) read "Java rest api using Jakarta EE" on this
|
|
197
|
+
# path alone. The API framing this exists to deliver is unchanged; only the
|
|
198
|
+
# claim narrows to what the annotations attest.
|
|
199
|
+
if not any(f.name == "JAX-RS" for f in frameworks) and any(
|
|
195
200
|
ep.kind in ("jax_rs_controller", "jax_rs_provider") for ep in entry_points
|
|
196
201
|
):
|
|
197
202
|
frameworks.append(FrameworkDetection(
|
|
198
|
-
name="
|
|
203
|
+
name="JAX-RS", source="annotation", detected_via=["jax_rs_endpoints"],
|
|
199
204
|
))
|
|
200
205
|
|
|
201
206
|
transactional_classes = self._collect_transactional_classes(context, all_paths)
|
|
@@ -436,15 +441,27 @@ class JavaDetector(AbstractDetector):
|
|
|
436
441
|
frameworks.append(FrameworkDetection(name="Micronaut", source=source))
|
|
437
442
|
if "io.vertx" in text or "vertx" in text:
|
|
438
443
|
frameworks.append(FrameworkDetection(name="Vert.x", source=source))
|
|
444
|
+
# P1-C: name the spec that is actually evidenced, not the platform that
|
|
445
|
+
# contains it. "Jakarta EE" is a platform of ~40 specs; a JAX-RS coordinate
|
|
446
|
+
# attests ONE of them. Claiming the platform from it told neo4j (a graph
|
|
447
|
+
# engine with an HTTP API) and jobrunr (a job library with RESTEasy on the
|
|
448
|
+
# test path) that they were Jakarta EE applications.
|
|
449
|
+
#
|
|
450
|
+
# Only an aggregate platform artifact — the coordinate that IS the platform
|
|
451
|
+
# umbrella — attests the platform itself. Counting distinct jakarta.* specs
|
|
452
|
+
# instead would misfire: Spring Boot 3 pulls jakarta.servlet, .validation and
|
|
453
|
+
# .persistence transitively (Broadleaf ships the first two), so any threshold
|
|
454
|
+
# low enough to catch a real EE app relabels every Boot 3 app.
|
|
455
|
+
if "javaee-api" in text: # javax.* umbrella (pre-EE 9)
|
|
456
|
+
frameworks.append(FrameworkDetection(name="Java EE", source=source))
|
|
457
|
+
if "jakartaee-api" in text or "jakarta.platform" in text or "jakarta.ee" in text:
|
|
458
|
+
frameworks.append(FrameworkDetection(name="Jakarta EE", source=source))
|
|
439
459
|
if (
|
|
440
|
-
"
|
|
441
|
-
or "
|
|
442
|
-
or "
|
|
443
|
-
or "javaee-api" in text # full Java EE platform
|
|
444
|
-
or "jakartaee-api" in text # full Jakarta EE platform
|
|
445
|
-
or "resteasy" in text # RESTEasy (JBoss JAX-RS impl)
|
|
460
|
+
"javax.ws.rs-api" in text # JAX-RS 2.x spec; excludes jsr311-api (1.x API-only jar)
|
|
461
|
+
or "jakarta.ws.rs" in text # Jakarta namespace (EE 9+)
|
|
462
|
+
or "resteasy" in text # RESTEasy — a JAX-RS implementation
|
|
446
463
|
):
|
|
447
|
-
frameworks.append(FrameworkDetection(name="
|
|
464
|
+
frameworks.append(FrameworkDetection(name="JAX-RS", source=source))
|
|
448
465
|
if "mybatis" in text:
|
|
449
466
|
frameworks.append(FrameworkDetection(name="MyBatis", source=source))
|
|
450
467
|
if "thymeleaf" in text:
|