osscodeiq 0.0.0__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.
- osscodeiq/__init__.py +0 -0
- osscodeiq/analyzer.py +467 -0
- osscodeiq/cache/__init__.py +0 -0
- osscodeiq/cache/hasher.py +23 -0
- osscodeiq/cache/store.py +300 -0
- osscodeiq/classifiers/__init__.py +0 -0
- osscodeiq/classifiers/layer_classifier.py +69 -0
- osscodeiq/cli.py +721 -0
- osscodeiq/config.py +113 -0
- osscodeiq/detectors/__init__.py +0 -0
- osscodeiq/detectors/auth/__init__.py +0 -0
- osscodeiq/detectors/auth/certificate_auth.py +139 -0
- osscodeiq/detectors/auth/ldap_auth.py +89 -0
- osscodeiq/detectors/auth/session_header_auth.py +120 -0
- osscodeiq/detectors/base.py +41 -0
- osscodeiq/detectors/config/__init__.py +0 -0
- osscodeiq/detectors/config/batch_structure.py +128 -0
- osscodeiq/detectors/config/cloudformation.py +183 -0
- osscodeiq/detectors/config/docker_compose.py +179 -0
- osscodeiq/detectors/config/github_actions.py +150 -0
- osscodeiq/detectors/config/gitlab_ci.py +216 -0
- osscodeiq/detectors/config/helm_chart.py +187 -0
- osscodeiq/detectors/config/ini_structure.py +101 -0
- osscodeiq/detectors/config/json_structure.py +72 -0
- osscodeiq/detectors/config/kubernetes.py +305 -0
- osscodeiq/detectors/config/kubernetes_rbac.py +212 -0
- osscodeiq/detectors/config/openapi.py +194 -0
- osscodeiq/detectors/config/package_json.py +99 -0
- osscodeiq/detectors/config/properties_detector.py +108 -0
- osscodeiq/detectors/config/pyproject_toml.py +169 -0
- osscodeiq/detectors/config/sql_structure.py +155 -0
- osscodeiq/detectors/config/toml_structure.py +93 -0
- osscodeiq/detectors/config/tsconfig_json.py +105 -0
- osscodeiq/detectors/config/yaml_structure.py +82 -0
- osscodeiq/detectors/cpp/__init__.py +0 -0
- osscodeiq/detectors/cpp/cpp_structures.py +192 -0
- osscodeiq/detectors/csharp/__init__.py +0 -0
- osscodeiq/detectors/csharp/csharp_efcore.py +184 -0
- osscodeiq/detectors/csharp/csharp_minimal_apis.py +156 -0
- osscodeiq/detectors/csharp/csharp_structures.py +317 -0
- osscodeiq/detectors/docs/__init__.py +0 -0
- osscodeiq/detectors/docs/markdown_structure.py +117 -0
- osscodeiq/detectors/frontend/__init__.py +0 -0
- osscodeiq/detectors/frontend/angular_components.py +177 -0
- osscodeiq/detectors/frontend/frontend_routes.py +259 -0
- osscodeiq/detectors/frontend/react_components.py +148 -0
- osscodeiq/detectors/frontend/svelte_components.py +84 -0
- osscodeiq/detectors/frontend/vue_components.py +150 -0
- osscodeiq/detectors/generic/__init__.py +1 -0
- osscodeiq/detectors/generic/imports_detector.py +413 -0
- osscodeiq/detectors/go/__init__.py +0 -0
- osscodeiq/detectors/go/go_orm.py +202 -0
- osscodeiq/detectors/go/go_structures.py +162 -0
- osscodeiq/detectors/go/go_web.py +157 -0
- osscodeiq/detectors/iac/__init__.py +0 -0
- osscodeiq/detectors/iac/bicep.py +135 -0
- osscodeiq/detectors/iac/dockerfile.py +182 -0
- osscodeiq/detectors/iac/terraform.py +188 -0
- osscodeiq/detectors/java/__init__.py +0 -0
- osscodeiq/detectors/java/azure_functions.py +424 -0
- osscodeiq/detectors/java/azure_messaging.py +350 -0
- osscodeiq/detectors/java/class_hierarchy.py +349 -0
- osscodeiq/detectors/java/config_def.py +82 -0
- osscodeiq/detectors/java/cosmos_db.py +105 -0
- osscodeiq/detectors/java/graphql_resolver.py +188 -0
- osscodeiq/detectors/java/grpc_service.py +142 -0
- osscodeiq/detectors/java/ibm_mq.py +178 -0
- osscodeiq/detectors/java/jaxrs.py +160 -0
- osscodeiq/detectors/java/jdbc.py +196 -0
- osscodeiq/detectors/java/jms.py +116 -0
- osscodeiq/detectors/java/jpa_entity.py +143 -0
- osscodeiq/detectors/java/kafka.py +113 -0
- osscodeiq/detectors/java/kafka_protocol.py +70 -0
- osscodeiq/detectors/java/micronaut.py +248 -0
- osscodeiq/detectors/java/module_deps.py +191 -0
- osscodeiq/detectors/java/public_api.py +206 -0
- osscodeiq/detectors/java/quarkus.py +176 -0
- osscodeiq/detectors/java/rabbitmq.py +150 -0
- osscodeiq/detectors/java/raw_sql.py +136 -0
- osscodeiq/detectors/java/repository.py +131 -0
- osscodeiq/detectors/java/rmi.py +129 -0
- osscodeiq/detectors/java/spring_events.py +117 -0
- osscodeiq/detectors/java/spring_rest.py +168 -0
- osscodeiq/detectors/java/spring_security.py +212 -0
- osscodeiq/detectors/java/tibco_ems.py +193 -0
- osscodeiq/detectors/java/websocket.py +188 -0
- osscodeiq/detectors/kotlin/__init__.py +0 -0
- osscodeiq/detectors/kotlin/kotlin_structures.py +124 -0
- osscodeiq/detectors/kotlin/ktor_routes.py +163 -0
- osscodeiq/detectors/proto/__init__.py +0 -0
- osscodeiq/detectors/proto/proto_structure.py +153 -0
- osscodeiq/detectors/python/__init__.py +0 -0
- osscodeiq/detectors/python/celery_tasks.py +88 -0
- osscodeiq/detectors/python/django_auth.py +132 -0
- osscodeiq/detectors/python/django_models.py +157 -0
- osscodeiq/detectors/python/django_views.py +74 -0
- osscodeiq/detectors/python/fastapi_auth.py +143 -0
- osscodeiq/detectors/python/fastapi_routes.py +68 -0
- osscodeiq/detectors/python/flask_routes.py +67 -0
- osscodeiq/detectors/python/kafka_python.py +175 -0
- osscodeiq/detectors/python/pydantic_models.py +115 -0
- osscodeiq/detectors/python/python_structures.py +234 -0
- osscodeiq/detectors/python/sqlalchemy_models.py +82 -0
- osscodeiq/detectors/registry.py +100 -0
- osscodeiq/detectors/rust/__init__.py +0 -0
- osscodeiq/detectors/rust/actix_web.py +234 -0
- osscodeiq/detectors/rust/rust_structures.py +174 -0
- osscodeiq/detectors/scala/__init__.py +0 -0
- osscodeiq/detectors/scala/scala_structures.py +128 -0
- osscodeiq/detectors/shell/__init__.py +0 -0
- osscodeiq/detectors/shell/bash_detector.py +127 -0
- osscodeiq/detectors/shell/powershell_detector.py +118 -0
- osscodeiq/detectors/typescript/__init__.py +0 -0
- osscodeiq/detectors/typescript/express_routes.py +55 -0
- osscodeiq/detectors/typescript/fastify_routes.py +156 -0
- osscodeiq/detectors/typescript/graphql_resolvers.py +100 -0
- osscodeiq/detectors/typescript/kafka_js.py +164 -0
- osscodeiq/detectors/typescript/mongoose_orm.py +151 -0
- osscodeiq/detectors/typescript/nestjs_controllers.py +99 -0
- osscodeiq/detectors/typescript/nestjs_guards.py +138 -0
- osscodeiq/detectors/typescript/passport_jwt.py +133 -0
- osscodeiq/detectors/typescript/prisma_orm.py +96 -0
- osscodeiq/detectors/typescript/remix_routes.py +160 -0
- osscodeiq/detectors/typescript/sequelize_orm.py +136 -0
- osscodeiq/detectors/typescript/typeorm_entities.py +86 -0
- osscodeiq/detectors/typescript/typescript_structures.py +185 -0
- osscodeiq/detectors/utils.py +49 -0
- osscodeiq/discovery/__init__.py +11 -0
- osscodeiq/discovery/change_detector.py +97 -0
- osscodeiq/discovery/file_discovery.py +342 -0
- osscodeiq/flow/__init__.py +0 -0
- osscodeiq/flow/engine.py +78 -0
- osscodeiq/flow/models.py +72 -0
- osscodeiq/flow/renderer.py +127 -0
- osscodeiq/flow/templates/interactive.html +252 -0
- osscodeiq/flow/vendor/cytoscape-dagre.min.js +8 -0
- osscodeiq/flow/vendor/cytoscape.min.js +32 -0
- osscodeiq/flow/vendor/dagre.min.js +3809 -0
- osscodeiq/flow/views.py +357 -0
- osscodeiq/graph/__init__.py +0 -0
- osscodeiq/graph/backend.py +52 -0
- osscodeiq/graph/backends/__init__.py +23 -0
- osscodeiq/graph/backends/kuzu.py +576 -0
- osscodeiq/graph/backends/networkx.py +135 -0
- osscodeiq/graph/backends/sqlite_backend.py +406 -0
- osscodeiq/graph/builder.py +297 -0
- osscodeiq/graph/query.py +228 -0
- osscodeiq/graph/store.py +183 -0
- osscodeiq/graph/views.py +231 -0
- osscodeiq/models/__init__.py +17 -0
- osscodeiq/models/graph.py +116 -0
- osscodeiq/output/__init__.py +0 -0
- osscodeiq/output/dot.py +171 -0
- osscodeiq/output/mermaid.py +160 -0
- osscodeiq/output/safety.py +58 -0
- osscodeiq/output/serializers.py +42 -0
- osscodeiq/parsing/__init__.py +5 -0
- osscodeiq/parsing/languages/__init__.py +0 -0
- osscodeiq/parsing/languages/base.py +23 -0
- osscodeiq/parsing/languages/java.py +68 -0
- osscodeiq/parsing/languages/python.py +57 -0
- osscodeiq/parsing/languages/typescript.py +95 -0
- osscodeiq/parsing/parser_manager.py +125 -0
- osscodeiq/parsing/structured/__init__.py +0 -0
- osscodeiq/parsing/structured/gradle_parser.py +78 -0
- osscodeiq/parsing/structured/json_parser.py +24 -0
- osscodeiq/parsing/structured/properties_parser.py +56 -0
- osscodeiq/parsing/structured/sql_parser.py +54 -0
- osscodeiq/parsing/structured/xml_parser.py +148 -0
- osscodeiq/parsing/structured/yaml_parser.py +38 -0
- osscodeiq/server/__init__.py +7 -0
- osscodeiq/server/app.py +53 -0
- osscodeiq/server/mcp_server.py +174 -0
- osscodeiq/server/middleware.py +16 -0
- osscodeiq/server/routes.py +184 -0
- osscodeiq/server/service.py +445 -0
- osscodeiq/server/templates/welcome.html +56 -0
- osscodeiq-0.0.0.dist-info/METADATA +30 -0
- osscodeiq-0.0.0.dist-info/RECORD +183 -0
- osscodeiq-0.0.0.dist-info/WHEEL +5 -0
- osscodeiq-0.0.0.dist-info/entry_points.txt +2 -0
- osscodeiq-0.0.0.dist-info/licenses/LICENSE +21 -0
- osscodeiq-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Thread-safe tree-sitter parser pool manager."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import queue
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import tree_sitter
|
|
10
|
+
|
|
11
|
+
from osscodeiq.parsing.languages.java import JavaLanguageSupport
|
|
12
|
+
from osscodeiq.parsing.languages.python import PythonLanguageSupport
|
|
13
|
+
from osscodeiq.parsing.languages.typescript import (
|
|
14
|
+
JavaScriptLanguageSupport,
|
|
15
|
+
TypeScriptLanguageSupport,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from osscodeiq.discovery.file_discovery import DiscoveredFile
|
|
20
|
+
from osscodeiq.parsing.languages.base import LanguageSupport
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Default pool size per language.
|
|
25
|
+
_DEFAULT_POOL_SIZE = 4
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ParserManager:
|
|
29
|
+
"""Manages a pool of tree-sitter parsers for thread-safe parsing."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, pool_size: int = _DEFAULT_POOL_SIZE) -> None:
|
|
32
|
+
self._pool_size = pool_size
|
|
33
|
+
self._languages: dict[str, LanguageSupport] = {}
|
|
34
|
+
self._ts_languages: dict[str, tree_sitter.Language] = {}
|
|
35
|
+
self._pools: dict[str, queue.Queue[tree_sitter.Parser]] = {}
|
|
36
|
+
self._query_cache: dict[tuple[str, str], tree_sitter.Query] = {}
|
|
37
|
+
|
|
38
|
+
# Auto-register built-in languages.
|
|
39
|
+
self._register_builtins()
|
|
40
|
+
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
# Public API
|
|
43
|
+
# ------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
def register_language(self, name: str, support: LanguageSupport) -> None:
|
|
46
|
+
"""Register a language and pre-populate its parser pool."""
|
|
47
|
+
self._languages[name] = support
|
|
48
|
+
ts_lang = support.get_language()
|
|
49
|
+
self._ts_languages[name] = ts_lang
|
|
50
|
+
self._pools[name] = self._create_pool(ts_lang)
|
|
51
|
+
|
|
52
|
+
def parse_file(
|
|
53
|
+
self, file: DiscoveredFile, content: bytes
|
|
54
|
+
) -> tree_sitter.Tree | None:
|
|
55
|
+
"""Parse *content* using the parser for *file*'s language.
|
|
56
|
+
|
|
57
|
+
Borrows a parser from the pool and returns it afterwards, making
|
|
58
|
+
this method safe to call from multiple threads concurrently.
|
|
59
|
+
"""
|
|
60
|
+
lang = file.language
|
|
61
|
+
pool = self._pools.get(lang)
|
|
62
|
+
if pool is None:
|
|
63
|
+
logger.debug("No parser registered for language %s", lang)
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
parser = pool.get()
|
|
67
|
+
try:
|
|
68
|
+
return parser.parse(content)
|
|
69
|
+
finally:
|
|
70
|
+
pool.put(parser)
|
|
71
|
+
|
|
72
|
+
def get_query(
|
|
73
|
+
self, language: str, query_name: str
|
|
74
|
+
) -> tree_sitter.Query | None:
|
|
75
|
+
"""Return a compiled tree-sitter Query, cached after first build."""
|
|
76
|
+
cache_key = (language, query_name)
|
|
77
|
+
if cache_key in self._query_cache:
|
|
78
|
+
return self._query_cache[cache_key]
|
|
79
|
+
|
|
80
|
+
support = self._languages.get(language)
|
|
81
|
+
if support is None:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
queries = support.get_queries()
|
|
85
|
+
source = queries.get(query_name)
|
|
86
|
+
if source is None:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
ts_lang = self._ts_languages[language]
|
|
90
|
+
compiled = tree_sitter.Query(ts_lang, source)
|
|
91
|
+
self._query_cache[cache_key] = compiled
|
|
92
|
+
return compiled
|
|
93
|
+
|
|
94
|
+
# ------------------------------------------------------------------
|
|
95
|
+
# Internals
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def _create_pool(
|
|
99
|
+
self, ts_lang: tree_sitter.Language
|
|
100
|
+
) -> queue.Queue[tree_sitter.Parser]:
|
|
101
|
+
pool: queue.Queue[tree_sitter.Parser] = queue.Queue(
|
|
102
|
+
maxsize=self._pool_size
|
|
103
|
+
)
|
|
104
|
+
for _ in range(self._pool_size):
|
|
105
|
+
parser = tree_sitter.Parser(ts_lang)
|
|
106
|
+
pool.put(parser)
|
|
107
|
+
return pool
|
|
108
|
+
|
|
109
|
+
def _register_builtins(self) -> None:
|
|
110
|
+
"""Register languages that ship with the package."""
|
|
111
|
+
builtins: list[LanguageSupport] = [
|
|
112
|
+
JavaLanguageSupport(), # type: ignore[list-item]
|
|
113
|
+
PythonLanguageSupport(), # type: ignore[list-item]
|
|
114
|
+
TypeScriptLanguageSupport(), # type: ignore[list-item]
|
|
115
|
+
JavaScriptLanguageSupport(), # type: ignore[list-item]
|
|
116
|
+
]
|
|
117
|
+
for support in builtins:
|
|
118
|
+
try:
|
|
119
|
+
self.register_language(support.name, support)
|
|
120
|
+
except Exception: # noqa: BLE001
|
|
121
|
+
logger.warning(
|
|
122
|
+
"Failed to register built-in language %s",
|
|
123
|
+
support.name,
|
|
124
|
+
exc_info=True,
|
|
125
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Regex-based Gradle build file parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
# Patterns for Gradle dependency declarations.
|
|
9
|
+
# Matches: implementation 'group:artifact:version'
|
|
10
|
+
# api "group:artifact:version"
|
|
11
|
+
# compile 'group:artifact:version'
|
|
12
|
+
# testImplementation("group:artifact:version")
|
|
13
|
+
_DEP_CONFIGS = (
|
|
14
|
+
"implementation",
|
|
15
|
+
"api",
|
|
16
|
+
"compile",
|
|
17
|
+
"compileOnly",
|
|
18
|
+
"runtimeOnly",
|
|
19
|
+
"testImplementation",
|
|
20
|
+
"testCompile",
|
|
21
|
+
"testRuntimeOnly",
|
|
22
|
+
"annotationProcessor",
|
|
23
|
+
"kapt",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_DEP_PATTERN = re.compile(
|
|
27
|
+
r"(?P<config>"
|
|
28
|
+
+ "|".join(_DEP_CONFIGS)
|
|
29
|
+
+ r")\s*[\(\s]['\"](?P<coords>[^'\"]+)['\"]",
|
|
30
|
+
re.MULTILINE,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Plugin patterns: id 'xxx' or id("xxx")
|
|
34
|
+
_PLUGIN_PATTERN = re.compile(
|
|
35
|
+
r"""id\s*[\(\s]['"](?P<plugin>[^'"]+)['"]""",
|
|
36
|
+
re.MULTILINE,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Group / version from the build file.
|
|
40
|
+
_GROUP_PATTERN = re.compile(r"""group\s*=\s*['"](?P<group>[^'"]+)['"]""")
|
|
41
|
+
_VERSION_PATTERN = re.compile(r"""version\s*=\s*['"](?P<version>[^'"]+)['"]""")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GradleParser:
|
|
45
|
+
"""Extracts dependencies and metadata from ``build.gradle`` files."""
|
|
46
|
+
|
|
47
|
+
def parse(self, content: bytes, file_path: str) -> dict[str, Any]:
|
|
48
|
+
"""Parse a Gradle build file and return structured data."""
|
|
49
|
+
text = content.decode("utf-8", errors="replace")
|
|
50
|
+
|
|
51
|
+
dependencies: list[dict[str, str]] = []
|
|
52
|
+
for m in _DEP_PATTERN.finditer(text):
|
|
53
|
+
config = m.group("config")
|
|
54
|
+
coords = m.group("coords")
|
|
55
|
+
parts = coords.split(":")
|
|
56
|
+
dep: dict[str, str] = {"configuration": config, "raw": coords}
|
|
57
|
+
if len(parts) >= 2:
|
|
58
|
+
dep["group"] = parts[0]
|
|
59
|
+
dep["artifact"] = parts[1]
|
|
60
|
+
if len(parts) >= 3:
|
|
61
|
+
dep["version"] = parts[2]
|
|
62
|
+
dependencies.append(dep)
|
|
63
|
+
|
|
64
|
+
plugins: list[str] = [
|
|
65
|
+
m.group("plugin") for m in _PLUGIN_PATTERN.finditer(text)
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
group_m = _GROUP_PATTERN.search(text)
|
|
69
|
+
version_m = _VERSION_PATTERN.search(text)
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
"type": "gradle",
|
|
73
|
+
"file": file_path,
|
|
74
|
+
"group": group_m.group("group") if group_m else None,
|
|
75
|
+
"version": version_m.group("version") if version_m else None,
|
|
76
|
+
"plugins": plugins,
|
|
77
|
+
"dependencies": dependencies,
|
|
78
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""JSON structured file parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class JsonParser:
|
|
10
|
+
"""Parses JSON files into structured dictionaries."""
|
|
11
|
+
|
|
12
|
+
def parse(self, content: bytes, file_path: str) -> dict[str, Any]:
|
|
13
|
+
"""Parse *content* as JSON and return a structured dict."""
|
|
14
|
+
try:
|
|
15
|
+
text = content.decode("utf-8", errors="replace")
|
|
16
|
+
data = json.loads(text)
|
|
17
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
18
|
+
return {"error": "invalid_json", "file": file_path, "detail": str(exc)}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
"type": "json",
|
|
22
|
+
"file": file_path,
|
|
23
|
+
"data": data,
|
|
24
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Java .properties file parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PropertiesParser:
|
|
9
|
+
"""Parses Java-style ``.properties`` files into structured dicts."""
|
|
10
|
+
|
|
11
|
+
def parse(self, content: bytes, file_path: str) -> dict[str, Any]:
|
|
12
|
+
"""Parse key=value property entries.
|
|
13
|
+
|
|
14
|
+
Handles ``=`` and ``:`` separators, comment lines (``#``, ``!``),
|
|
15
|
+
blank lines, and continuation lines ending with ``\\``.
|
|
16
|
+
"""
|
|
17
|
+
text = content.decode("utf-8", errors="replace")
|
|
18
|
+
properties: dict[str, str] = {}
|
|
19
|
+
|
|
20
|
+
logical_line = ""
|
|
21
|
+
for raw_line in text.splitlines():
|
|
22
|
+
# Handle continuation lines
|
|
23
|
+
if logical_line:
|
|
24
|
+
raw_line = raw_line.lstrip()
|
|
25
|
+
logical_line += raw_line
|
|
26
|
+
|
|
27
|
+
if logical_line.endswith("\\"):
|
|
28
|
+
logical_line = logical_line[:-1]
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
line = logical_line.strip()
|
|
32
|
+
logical_line = ""
|
|
33
|
+
|
|
34
|
+
if not line or line.startswith("#") or line.startswith("!"):
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
# Split on first unescaped = or :
|
|
38
|
+
sep_idx = -1
|
|
39
|
+
for i, ch in enumerate(line):
|
|
40
|
+
if ch in ("=", ":") and (i == 0 or line[i - 1] != "\\"):
|
|
41
|
+
sep_idx = i
|
|
42
|
+
break
|
|
43
|
+
|
|
44
|
+
if sep_idx == -1:
|
|
45
|
+
# Treat the whole line as a key with empty value
|
|
46
|
+
properties[line] = ""
|
|
47
|
+
else:
|
|
48
|
+
key = line[:sep_idx].rstrip()
|
|
49
|
+
value = line[sep_idx + 1 :].lstrip()
|
|
50
|
+
properties[key] = value
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
"type": "properties",
|
|
54
|
+
"file": file_path,
|
|
55
|
+
"data": properties,
|
|
56
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""SQL migration file parser using sqlparse."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import sqlparse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SqlParser:
|
|
12
|
+
"""Parses SQL migration files (Flyway, Liquibase) to extract DDL statements."""
|
|
13
|
+
|
|
14
|
+
_TABLE_NAME_RE = re.compile(
|
|
15
|
+
r"(?:CREATE|ALTER|DROP)\s+TABLE\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?[`\"]?(\w+)[`\"]?",
|
|
16
|
+
re.IGNORECASE,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def parse(self, content: bytes, file_path: str) -> dict[str, Any]:
|
|
20
|
+
"""Parse SQL content and extract DDL information."""
|
|
21
|
+
text = content.decode("utf-8", errors="replace")
|
|
22
|
+
statements = sqlparse.parse(text)
|
|
23
|
+
|
|
24
|
+
tables_created: list[str] = []
|
|
25
|
+
tables_altered: list[str] = []
|
|
26
|
+
tables_dropped: list[str] = []
|
|
27
|
+
raw_statements: list[str] = []
|
|
28
|
+
|
|
29
|
+
for stmt in statements:
|
|
30
|
+
stmt_text = str(stmt).strip()
|
|
31
|
+
if not stmt_text:
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
stmt_type = stmt.get_type()
|
|
35
|
+
raw_statements.append(stmt_text)
|
|
36
|
+
|
|
37
|
+
for match in self._TABLE_NAME_RE.finditer(stmt_text):
|
|
38
|
+
table_name = match.group(1)
|
|
39
|
+
upper = stmt_text.upper().lstrip()
|
|
40
|
+
if upper.startswith("CREATE"):
|
|
41
|
+
tables_created.append(table_name)
|
|
42
|
+
elif upper.startswith("ALTER"):
|
|
43
|
+
tables_altered.append(table_name)
|
|
44
|
+
elif upper.startswith("DROP"):
|
|
45
|
+
tables_dropped.append(table_name)
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
"type": "sql",
|
|
49
|
+
"file": file_path,
|
|
50
|
+
"tables_created": tables_created,
|
|
51
|
+
"tables_altered": tables_altered,
|
|
52
|
+
"tables_dropped": tables_dropped,
|
|
53
|
+
"statement_count": len(raw_statements),
|
|
54
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""XML structured file parser with special handling for Maven and Spring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from lxml import etree
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class XmlParser:
|
|
11
|
+
"""Parses XML files into structured dictionaries.
|
|
12
|
+
|
|
13
|
+
Provides specialised extraction for:
|
|
14
|
+
- Maven ``pom.xml`` (groupId, artifactId, dependencies, modules)
|
|
15
|
+
- Spring XML configuration (beans, component-scan)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def parse(self, content: bytes, file_path: str) -> dict[str, Any]:
|
|
19
|
+
"""Parse *content* and return a structured dict."""
|
|
20
|
+
try:
|
|
21
|
+
parser = etree.XMLParser(resolve_entities=False, no_network=True)
|
|
22
|
+
root = etree.fromstring(content, parser)
|
|
23
|
+
except etree.XMLSyntaxError:
|
|
24
|
+
return {"error": "invalid_xml", "file": file_path}
|
|
25
|
+
|
|
26
|
+
file_lower = file_path.rsplit("/", 1)[-1].lower()
|
|
27
|
+
|
|
28
|
+
if file_lower == "pom.xml":
|
|
29
|
+
return self._parse_pom(root, file_path)
|
|
30
|
+
|
|
31
|
+
# Detect Spring XML config by namespace or root tag.
|
|
32
|
+
root_tag = etree.QName(root.tag).localname if root.tag else ""
|
|
33
|
+
ns = root.nsmap.get(None, "")
|
|
34
|
+
if root_tag == "beans" or "springframework" in ns:
|
|
35
|
+
return self._parse_spring_xml(root, file_path)
|
|
36
|
+
|
|
37
|
+
# Generic XML: return tag tree summary.
|
|
38
|
+
return {
|
|
39
|
+
"type": "xml",
|
|
40
|
+
"file": file_path,
|
|
41
|
+
"root_tag": root_tag,
|
|
42
|
+
"namespaces": dict(root.nsmap),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
# Maven POM parsing
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
def _parse_pom(
|
|
50
|
+
self, root: etree._Element, file_path: str
|
|
51
|
+
) -> dict[str, Any]:
|
|
52
|
+
ns = root.nsmap.get(None, "")
|
|
53
|
+
prefix = f"{{{ns}}}" if ns else ""
|
|
54
|
+
|
|
55
|
+
def _text(parent: etree._Element, tag: str) -> str | None:
|
|
56
|
+
el = parent.find(f"{prefix}{tag}")
|
|
57
|
+
return el.text.strip() if el is not None and el.text else None
|
|
58
|
+
|
|
59
|
+
group_id = _text(root, "groupId")
|
|
60
|
+
artifact_id = _text(root, "artifactId")
|
|
61
|
+
version = _text(root, "version")
|
|
62
|
+
packaging = _text(root, "packaging")
|
|
63
|
+
|
|
64
|
+
# Parent info
|
|
65
|
+
parent_el = root.find(f"{prefix}parent")
|
|
66
|
+
parent: dict[str, str | None] | None = None
|
|
67
|
+
if parent_el is not None:
|
|
68
|
+
parent = {
|
|
69
|
+
"groupId": _text(parent_el, "groupId"),
|
|
70
|
+
"artifactId": _text(parent_el, "artifactId"),
|
|
71
|
+
"version": _text(parent_el, "version"),
|
|
72
|
+
}
|
|
73
|
+
if group_id is None:
|
|
74
|
+
group_id = parent.get("groupId")
|
|
75
|
+
|
|
76
|
+
# Dependencies
|
|
77
|
+
deps: list[dict[str, str | None]] = []
|
|
78
|
+
deps_el = root.find(f"{prefix}dependencies")
|
|
79
|
+
if deps_el is not None:
|
|
80
|
+
for dep in deps_el.findall(f"{prefix}dependency"):
|
|
81
|
+
deps.append(
|
|
82
|
+
{
|
|
83
|
+
"groupId": _text(dep, "groupId"),
|
|
84
|
+
"artifactId": _text(dep, "artifactId"),
|
|
85
|
+
"version": _text(dep, "version"),
|
|
86
|
+
"scope": _text(dep, "scope"),
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Modules
|
|
91
|
+
modules: list[str] = []
|
|
92
|
+
modules_el = root.find(f"{prefix}modules")
|
|
93
|
+
if modules_el is not None:
|
|
94
|
+
for mod in modules_el.findall(f"{prefix}module"):
|
|
95
|
+
if mod.text:
|
|
96
|
+
modules.append(mod.text.strip())
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
"type": "pom",
|
|
100
|
+
"file": file_path,
|
|
101
|
+
"groupId": group_id,
|
|
102
|
+
"artifactId": artifact_id,
|
|
103
|
+
"version": version,
|
|
104
|
+
"packaging": packaging,
|
|
105
|
+
"parent": parent,
|
|
106
|
+
"dependencies": deps,
|
|
107
|
+
"modules": modules,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
# Spring XML config parsing
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
def _parse_spring_xml(
|
|
115
|
+
self, root: etree._Element, file_path: str
|
|
116
|
+
) -> dict[str, Any]:
|
|
117
|
+
ns = root.nsmap.get(None, "")
|
|
118
|
+
prefix = f"{{{ns}}}" if ns else ""
|
|
119
|
+
|
|
120
|
+
beans: list[dict[str, str | None]] = []
|
|
121
|
+
for bean in root.iter(f"{prefix}bean"):
|
|
122
|
+
beans.append(
|
|
123
|
+
{
|
|
124
|
+
"id": bean.get("id"),
|
|
125
|
+
"class": bean.get("class"),
|
|
126
|
+
"scope": bean.get("scope"),
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# context:component-scan
|
|
131
|
+
component_scans: list[str] = []
|
|
132
|
+
ctx_ns = None
|
|
133
|
+
for pfx, uri in root.nsmap.items():
|
|
134
|
+
if "context" in (uri or ""):
|
|
135
|
+
ctx_ns = uri
|
|
136
|
+
break
|
|
137
|
+
if ctx_ns:
|
|
138
|
+
for scan in root.iter(f"{{{ctx_ns}}}component-scan"):
|
|
139
|
+
pkg = scan.get("base-package")
|
|
140
|
+
if pkg:
|
|
141
|
+
component_scans.append(pkg)
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
"type": "spring_xml",
|
|
145
|
+
"file": file_path,
|
|
146
|
+
"beans": beans,
|
|
147
|
+
"component_scans": component_scans,
|
|
148
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""YAML structured file parser with multi-document support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class YamlParser:
|
|
11
|
+
"""Parses YAML files into structured dictionaries."""
|
|
12
|
+
|
|
13
|
+
def parse(self, content: bytes, file_path: str) -> dict[str, Any]:
|
|
14
|
+
"""Parse *content* as YAML.
|
|
15
|
+
|
|
16
|
+
Supports multi-document YAML files (``---`` separators). When
|
|
17
|
+
multiple documents are present the result contains a ``documents``
|
|
18
|
+
list; single-document files return the document directly under a
|
|
19
|
+
``data`` key.
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
text = content.decode("utf-8", errors="replace")
|
|
23
|
+
docs = list(yaml.safe_load_all(text))
|
|
24
|
+
except yaml.YAMLError as exc:
|
|
25
|
+
return {"error": "invalid_yaml", "file": file_path, "detail": str(exc)}
|
|
26
|
+
|
|
27
|
+
if len(docs) == 1:
|
|
28
|
+
return {
|
|
29
|
+
"type": "yaml",
|
|
30
|
+
"file": file_path,
|
|
31
|
+
"data": docs[0],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
"type": "yaml_multi",
|
|
36
|
+
"file": file_path,
|
|
37
|
+
"documents": docs,
|
|
38
|
+
}
|
osscodeiq/server/app.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""FastAPI application assembly — mounts REST API, MCP server, and welcome page."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from fastapi.responses import HTMLResponse
|
|
9
|
+
|
|
10
|
+
from osscodeiq.server.middleware import AuthMiddleware
|
|
11
|
+
from osscodeiq.server.mcp_server import get_mcp_app, set_service
|
|
12
|
+
from osscodeiq.server.routes import create_router
|
|
13
|
+
from osscodeiq.server.service import CodeIQService
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_app(
|
|
17
|
+
codebase_path: Path = Path("."),
|
|
18
|
+
backend: str = "networkx",
|
|
19
|
+
config_path: Path | None = None,
|
|
20
|
+
) -> FastAPI:
|
|
21
|
+
"""Create and configure the unified OSSCodeIQ server."""
|
|
22
|
+
service = CodeIQService(
|
|
23
|
+
path=codebase_path, backend=backend, config_path=config_path
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Set up MCP server
|
|
27
|
+
set_service(service)
|
|
28
|
+
mcp_app = get_mcp_app()
|
|
29
|
+
|
|
30
|
+
# Create FastAPI with MCP lifespan
|
|
31
|
+
app = FastAPI(
|
|
32
|
+
title="OSSCodeIQ",
|
|
33
|
+
description="OSSCodeIQ — graph queries, flow diagrams, and codebase analysis",
|
|
34
|
+
lifespan=mcp_app.lifespan,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Auth middleware stub (no-op, ready for future auth)
|
|
38
|
+
app.add_middleware(AuthMiddleware)
|
|
39
|
+
|
|
40
|
+
# Mount MCP at /mcp (streamable HTTP)
|
|
41
|
+
app.mount("/mcp", mcp_app)
|
|
42
|
+
|
|
43
|
+
# Include REST routes at /api
|
|
44
|
+
router = create_router(service)
|
|
45
|
+
app.include_router(router)
|
|
46
|
+
|
|
47
|
+
# Welcome page at /
|
|
48
|
+
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
|
|
49
|
+
async def welcome():
|
|
50
|
+
template_path = Path(__file__).parent / "templates" / "welcome.html"
|
|
51
|
+
return HTMLResponse(template_path.read_text(encoding="utf-8"))
|
|
52
|
+
|
|
53
|
+
return app
|