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.
Files changed (183) hide show
  1. osscodeiq/__init__.py +0 -0
  2. osscodeiq/analyzer.py +467 -0
  3. osscodeiq/cache/__init__.py +0 -0
  4. osscodeiq/cache/hasher.py +23 -0
  5. osscodeiq/cache/store.py +300 -0
  6. osscodeiq/classifiers/__init__.py +0 -0
  7. osscodeiq/classifiers/layer_classifier.py +69 -0
  8. osscodeiq/cli.py +721 -0
  9. osscodeiq/config.py +113 -0
  10. osscodeiq/detectors/__init__.py +0 -0
  11. osscodeiq/detectors/auth/__init__.py +0 -0
  12. osscodeiq/detectors/auth/certificate_auth.py +139 -0
  13. osscodeiq/detectors/auth/ldap_auth.py +89 -0
  14. osscodeiq/detectors/auth/session_header_auth.py +120 -0
  15. osscodeiq/detectors/base.py +41 -0
  16. osscodeiq/detectors/config/__init__.py +0 -0
  17. osscodeiq/detectors/config/batch_structure.py +128 -0
  18. osscodeiq/detectors/config/cloudformation.py +183 -0
  19. osscodeiq/detectors/config/docker_compose.py +179 -0
  20. osscodeiq/detectors/config/github_actions.py +150 -0
  21. osscodeiq/detectors/config/gitlab_ci.py +216 -0
  22. osscodeiq/detectors/config/helm_chart.py +187 -0
  23. osscodeiq/detectors/config/ini_structure.py +101 -0
  24. osscodeiq/detectors/config/json_structure.py +72 -0
  25. osscodeiq/detectors/config/kubernetes.py +305 -0
  26. osscodeiq/detectors/config/kubernetes_rbac.py +212 -0
  27. osscodeiq/detectors/config/openapi.py +194 -0
  28. osscodeiq/detectors/config/package_json.py +99 -0
  29. osscodeiq/detectors/config/properties_detector.py +108 -0
  30. osscodeiq/detectors/config/pyproject_toml.py +169 -0
  31. osscodeiq/detectors/config/sql_structure.py +155 -0
  32. osscodeiq/detectors/config/toml_structure.py +93 -0
  33. osscodeiq/detectors/config/tsconfig_json.py +105 -0
  34. osscodeiq/detectors/config/yaml_structure.py +82 -0
  35. osscodeiq/detectors/cpp/__init__.py +0 -0
  36. osscodeiq/detectors/cpp/cpp_structures.py +192 -0
  37. osscodeiq/detectors/csharp/__init__.py +0 -0
  38. osscodeiq/detectors/csharp/csharp_efcore.py +184 -0
  39. osscodeiq/detectors/csharp/csharp_minimal_apis.py +156 -0
  40. osscodeiq/detectors/csharp/csharp_structures.py +317 -0
  41. osscodeiq/detectors/docs/__init__.py +0 -0
  42. osscodeiq/detectors/docs/markdown_structure.py +117 -0
  43. osscodeiq/detectors/frontend/__init__.py +0 -0
  44. osscodeiq/detectors/frontend/angular_components.py +177 -0
  45. osscodeiq/detectors/frontend/frontend_routes.py +259 -0
  46. osscodeiq/detectors/frontend/react_components.py +148 -0
  47. osscodeiq/detectors/frontend/svelte_components.py +84 -0
  48. osscodeiq/detectors/frontend/vue_components.py +150 -0
  49. osscodeiq/detectors/generic/__init__.py +1 -0
  50. osscodeiq/detectors/generic/imports_detector.py +413 -0
  51. osscodeiq/detectors/go/__init__.py +0 -0
  52. osscodeiq/detectors/go/go_orm.py +202 -0
  53. osscodeiq/detectors/go/go_structures.py +162 -0
  54. osscodeiq/detectors/go/go_web.py +157 -0
  55. osscodeiq/detectors/iac/__init__.py +0 -0
  56. osscodeiq/detectors/iac/bicep.py +135 -0
  57. osscodeiq/detectors/iac/dockerfile.py +182 -0
  58. osscodeiq/detectors/iac/terraform.py +188 -0
  59. osscodeiq/detectors/java/__init__.py +0 -0
  60. osscodeiq/detectors/java/azure_functions.py +424 -0
  61. osscodeiq/detectors/java/azure_messaging.py +350 -0
  62. osscodeiq/detectors/java/class_hierarchy.py +349 -0
  63. osscodeiq/detectors/java/config_def.py +82 -0
  64. osscodeiq/detectors/java/cosmos_db.py +105 -0
  65. osscodeiq/detectors/java/graphql_resolver.py +188 -0
  66. osscodeiq/detectors/java/grpc_service.py +142 -0
  67. osscodeiq/detectors/java/ibm_mq.py +178 -0
  68. osscodeiq/detectors/java/jaxrs.py +160 -0
  69. osscodeiq/detectors/java/jdbc.py +196 -0
  70. osscodeiq/detectors/java/jms.py +116 -0
  71. osscodeiq/detectors/java/jpa_entity.py +143 -0
  72. osscodeiq/detectors/java/kafka.py +113 -0
  73. osscodeiq/detectors/java/kafka_protocol.py +70 -0
  74. osscodeiq/detectors/java/micronaut.py +248 -0
  75. osscodeiq/detectors/java/module_deps.py +191 -0
  76. osscodeiq/detectors/java/public_api.py +206 -0
  77. osscodeiq/detectors/java/quarkus.py +176 -0
  78. osscodeiq/detectors/java/rabbitmq.py +150 -0
  79. osscodeiq/detectors/java/raw_sql.py +136 -0
  80. osscodeiq/detectors/java/repository.py +131 -0
  81. osscodeiq/detectors/java/rmi.py +129 -0
  82. osscodeiq/detectors/java/spring_events.py +117 -0
  83. osscodeiq/detectors/java/spring_rest.py +168 -0
  84. osscodeiq/detectors/java/spring_security.py +212 -0
  85. osscodeiq/detectors/java/tibco_ems.py +193 -0
  86. osscodeiq/detectors/java/websocket.py +188 -0
  87. osscodeiq/detectors/kotlin/__init__.py +0 -0
  88. osscodeiq/detectors/kotlin/kotlin_structures.py +124 -0
  89. osscodeiq/detectors/kotlin/ktor_routes.py +163 -0
  90. osscodeiq/detectors/proto/__init__.py +0 -0
  91. osscodeiq/detectors/proto/proto_structure.py +153 -0
  92. osscodeiq/detectors/python/__init__.py +0 -0
  93. osscodeiq/detectors/python/celery_tasks.py +88 -0
  94. osscodeiq/detectors/python/django_auth.py +132 -0
  95. osscodeiq/detectors/python/django_models.py +157 -0
  96. osscodeiq/detectors/python/django_views.py +74 -0
  97. osscodeiq/detectors/python/fastapi_auth.py +143 -0
  98. osscodeiq/detectors/python/fastapi_routes.py +68 -0
  99. osscodeiq/detectors/python/flask_routes.py +67 -0
  100. osscodeiq/detectors/python/kafka_python.py +175 -0
  101. osscodeiq/detectors/python/pydantic_models.py +115 -0
  102. osscodeiq/detectors/python/python_structures.py +234 -0
  103. osscodeiq/detectors/python/sqlalchemy_models.py +82 -0
  104. osscodeiq/detectors/registry.py +100 -0
  105. osscodeiq/detectors/rust/__init__.py +0 -0
  106. osscodeiq/detectors/rust/actix_web.py +234 -0
  107. osscodeiq/detectors/rust/rust_structures.py +174 -0
  108. osscodeiq/detectors/scala/__init__.py +0 -0
  109. osscodeiq/detectors/scala/scala_structures.py +128 -0
  110. osscodeiq/detectors/shell/__init__.py +0 -0
  111. osscodeiq/detectors/shell/bash_detector.py +127 -0
  112. osscodeiq/detectors/shell/powershell_detector.py +118 -0
  113. osscodeiq/detectors/typescript/__init__.py +0 -0
  114. osscodeiq/detectors/typescript/express_routes.py +55 -0
  115. osscodeiq/detectors/typescript/fastify_routes.py +156 -0
  116. osscodeiq/detectors/typescript/graphql_resolvers.py +100 -0
  117. osscodeiq/detectors/typescript/kafka_js.py +164 -0
  118. osscodeiq/detectors/typescript/mongoose_orm.py +151 -0
  119. osscodeiq/detectors/typescript/nestjs_controllers.py +99 -0
  120. osscodeiq/detectors/typescript/nestjs_guards.py +138 -0
  121. osscodeiq/detectors/typescript/passport_jwt.py +133 -0
  122. osscodeiq/detectors/typescript/prisma_orm.py +96 -0
  123. osscodeiq/detectors/typescript/remix_routes.py +160 -0
  124. osscodeiq/detectors/typescript/sequelize_orm.py +136 -0
  125. osscodeiq/detectors/typescript/typeorm_entities.py +86 -0
  126. osscodeiq/detectors/typescript/typescript_structures.py +185 -0
  127. osscodeiq/detectors/utils.py +49 -0
  128. osscodeiq/discovery/__init__.py +11 -0
  129. osscodeiq/discovery/change_detector.py +97 -0
  130. osscodeiq/discovery/file_discovery.py +342 -0
  131. osscodeiq/flow/__init__.py +0 -0
  132. osscodeiq/flow/engine.py +78 -0
  133. osscodeiq/flow/models.py +72 -0
  134. osscodeiq/flow/renderer.py +127 -0
  135. osscodeiq/flow/templates/interactive.html +252 -0
  136. osscodeiq/flow/vendor/cytoscape-dagre.min.js +8 -0
  137. osscodeiq/flow/vendor/cytoscape.min.js +32 -0
  138. osscodeiq/flow/vendor/dagre.min.js +3809 -0
  139. osscodeiq/flow/views.py +357 -0
  140. osscodeiq/graph/__init__.py +0 -0
  141. osscodeiq/graph/backend.py +52 -0
  142. osscodeiq/graph/backends/__init__.py +23 -0
  143. osscodeiq/graph/backends/kuzu.py +576 -0
  144. osscodeiq/graph/backends/networkx.py +135 -0
  145. osscodeiq/graph/backends/sqlite_backend.py +406 -0
  146. osscodeiq/graph/builder.py +297 -0
  147. osscodeiq/graph/query.py +228 -0
  148. osscodeiq/graph/store.py +183 -0
  149. osscodeiq/graph/views.py +231 -0
  150. osscodeiq/models/__init__.py +17 -0
  151. osscodeiq/models/graph.py +116 -0
  152. osscodeiq/output/__init__.py +0 -0
  153. osscodeiq/output/dot.py +171 -0
  154. osscodeiq/output/mermaid.py +160 -0
  155. osscodeiq/output/safety.py +58 -0
  156. osscodeiq/output/serializers.py +42 -0
  157. osscodeiq/parsing/__init__.py +5 -0
  158. osscodeiq/parsing/languages/__init__.py +0 -0
  159. osscodeiq/parsing/languages/base.py +23 -0
  160. osscodeiq/parsing/languages/java.py +68 -0
  161. osscodeiq/parsing/languages/python.py +57 -0
  162. osscodeiq/parsing/languages/typescript.py +95 -0
  163. osscodeiq/parsing/parser_manager.py +125 -0
  164. osscodeiq/parsing/structured/__init__.py +0 -0
  165. osscodeiq/parsing/structured/gradle_parser.py +78 -0
  166. osscodeiq/parsing/structured/json_parser.py +24 -0
  167. osscodeiq/parsing/structured/properties_parser.py +56 -0
  168. osscodeiq/parsing/structured/sql_parser.py +54 -0
  169. osscodeiq/parsing/structured/xml_parser.py +148 -0
  170. osscodeiq/parsing/structured/yaml_parser.py +38 -0
  171. osscodeiq/server/__init__.py +7 -0
  172. osscodeiq/server/app.py +53 -0
  173. osscodeiq/server/mcp_server.py +174 -0
  174. osscodeiq/server/middleware.py +16 -0
  175. osscodeiq/server/routes.py +184 -0
  176. osscodeiq/server/service.py +445 -0
  177. osscodeiq/server/templates/welcome.html +56 -0
  178. osscodeiq-0.0.0.dist-info/METADATA +30 -0
  179. osscodeiq-0.0.0.dist-info/RECORD +183 -0
  180. osscodeiq-0.0.0.dist-info/WHEEL +5 -0
  181. osscodeiq-0.0.0.dist-info/entry_points.txt +2 -0
  182. osscodeiq-0.0.0.dist-info/licenses/LICENSE +21 -0
  183. osscodeiq-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,194 @@
1
+ """Detector for OpenAPI 3.x and Swagger 2.0 specification files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
6
+ from osscodeiq.models.graph import (
7
+ EdgeKind,
8
+ GraphEdge,
9
+ GraphNode,
10
+ NodeKind,
11
+ SourceLocation,
12
+ )
13
+
14
+
15
+ class OpenApiDetector:
16
+ """Detects API endpoints and schemas from OpenAPI/Swagger specifications."""
17
+
18
+ name: str = "openapi"
19
+ supported_languages: tuple[str, ...] = ("json", "yaml")
20
+
21
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
22
+ result = DetectorResult()
23
+
24
+ data = ctx.parsed_data
25
+ if not isinstance(data, dict) or not isinstance(data.get("data"), dict):
26
+ return result
27
+
28
+ spec = data["data"]
29
+
30
+ # Only trigger if this is an OpenAPI or Swagger spec
31
+ if "openapi" not in spec and "swagger" not in spec:
32
+ return result
33
+
34
+ filepath = ctx.file_path
35
+ config_id = f"api:{filepath}"
36
+
37
+ # Extract info metadata
38
+ info = spec.get("info") if isinstance(spec.get("info"), dict) else {}
39
+ api_title = info.get("title", filepath) if isinstance(info.get("title"), str) else filepath
40
+ api_version = info.get("version", "") if isinstance(info.get("version"), str) else ""
41
+ spec_version = spec.get("openapi") or spec.get("swagger") or ""
42
+
43
+ # CONFIG_FILE node for the spec
44
+ result.nodes.append(GraphNode(
45
+ id=config_id,
46
+ kind=NodeKind.CONFIG_FILE,
47
+ label=api_title,
48
+ fqn=filepath,
49
+ module=ctx.module_name,
50
+ location=SourceLocation(file_path=filepath),
51
+ properties={
52
+ "config_type": "openapi",
53
+ "api_title": api_title,
54
+ "api_version": api_version,
55
+ "spec_version": str(spec_version),
56
+ },
57
+ ))
58
+
59
+ # ENDPOINT nodes for each path + method combination
60
+ paths = spec.get("paths")
61
+ if isinstance(paths, dict):
62
+ for path, path_item in paths.items():
63
+ if not isinstance(path, str) or not isinstance(path_item, dict):
64
+ continue
65
+ for method, operation in path_item.items():
66
+ if not isinstance(method, str):
67
+ continue
68
+ # Skip non-HTTP-method keys (e.g. "parameters", "summary")
69
+ if method.lower() not in (
70
+ "get", "post", "put", "patch", "delete",
71
+ "head", "options", "trace",
72
+ ):
73
+ continue
74
+ method_upper = method.upper()
75
+ endpoint_id = f"api:{filepath}:{method.lower()}:{path}"
76
+ props: dict[str, object] = {
77
+ "http_method": method_upper,
78
+ "path": path,
79
+ }
80
+ if isinstance(operation, dict):
81
+ op_id = operation.get("operationId")
82
+ if isinstance(op_id, str):
83
+ props["operation_id"] = op_id
84
+ summary = operation.get("summary")
85
+ if isinstance(summary, str):
86
+ props["summary"] = summary
87
+
88
+ result.nodes.append(GraphNode(
89
+ id=endpoint_id,
90
+ kind=NodeKind.ENDPOINT,
91
+ label=f"{method_upper} {path}",
92
+ module=ctx.module_name,
93
+ location=SourceLocation(file_path=filepath),
94
+ properties=props,
95
+ ))
96
+ result.edges.append(GraphEdge(
97
+ source=config_id,
98
+ target=endpoint_id,
99
+ kind=EdgeKind.CONTAINS,
100
+ label=f"{api_title} contains {method_upper} {path}",
101
+ ))
102
+
103
+ # ENTITY nodes for schemas — OpenAPI 3.x and Swagger 2.0
104
+ schemas = _extract_schemas(spec)
105
+ for schema_name, schema_def in schemas.items():
106
+ if not isinstance(schema_name, str):
107
+ continue
108
+ schema_id = f"api:{filepath}:schema:{schema_name}"
109
+ schema_props: dict[str, object] = {"schema_name": schema_name}
110
+ if isinstance(schema_def, dict):
111
+ schema_type = schema_def.get("type")
112
+ if isinstance(schema_type, str):
113
+ schema_props["schema_type"] = schema_type
114
+
115
+ result.nodes.append(GraphNode(
116
+ id=schema_id,
117
+ kind=NodeKind.ENTITY,
118
+ label=schema_name,
119
+ module=ctx.module_name,
120
+ location=SourceLocation(file_path=filepath),
121
+ properties=schema_props,
122
+ ))
123
+ result.edges.append(GraphEdge(
124
+ source=config_id,
125
+ target=schema_id,
126
+ kind=EdgeKind.CONTAINS,
127
+ label=f"{api_title} defines schema {schema_name}",
128
+ ))
129
+
130
+ # DEPENDS_ON edges for $ref references within this schema
131
+ if isinstance(schema_def, dict):
132
+ refs = _collect_refs(schema_def)
133
+ for ref in refs:
134
+ ref_name = _ref_to_schema_name(ref)
135
+ if ref_name and ref_name != schema_name and ref_name in schemas:
136
+ result.edges.append(GraphEdge(
137
+ source=schema_id,
138
+ target=f"api:{filepath}:schema:{ref_name}",
139
+ kind=EdgeKind.DEPENDS_ON,
140
+ label=f"{schema_name} references {ref_name}",
141
+ ))
142
+
143
+ return result
144
+
145
+
146
+ def _extract_schemas(spec: dict) -> dict:
147
+ """Extract schema definitions from both OpenAPI 3.x and Swagger 2.0."""
148
+ # OpenAPI 3.x: components.schemas
149
+ components = spec.get("components")
150
+ if isinstance(components, dict):
151
+ schemas = components.get("schemas")
152
+ if isinstance(schemas, dict):
153
+ return schemas
154
+
155
+ # Swagger 2.0: definitions
156
+ definitions = spec.get("definitions")
157
+ if isinstance(definitions, dict):
158
+ return definitions
159
+
160
+ return {}
161
+
162
+
163
+ def _collect_refs(obj: dict | list, _seen: set | None = None) -> list[str]:
164
+ """Recursively collect all $ref values from a schema definition."""
165
+ if _seen is None:
166
+ _seen = set()
167
+ refs: list[str] = []
168
+ obj_id = id(obj)
169
+ if obj_id in _seen:
170
+ return refs
171
+ _seen.add(obj_id)
172
+
173
+ if isinstance(obj, dict):
174
+ ref = obj.get("$ref")
175
+ if isinstance(ref, str):
176
+ refs.append(ref)
177
+ for value in obj.values():
178
+ if isinstance(value, (dict, list)):
179
+ refs.extend(_collect_refs(value, _seen))
180
+ elif isinstance(obj, list):
181
+ for item in obj:
182
+ if isinstance(item, (dict, list)):
183
+ refs.extend(_collect_refs(item, _seen))
184
+ return refs
185
+
186
+
187
+ def _ref_to_schema_name(ref: str) -> str | None:
188
+ """Extract a schema name from a $ref string like '#/components/schemas/User'."""
189
+ if not ref.startswith("#/"):
190
+ return None
191
+ parts = ref.split("/")
192
+ if len(parts) >= 2:
193
+ return parts[-1]
194
+ return None
@@ -0,0 +1,99 @@
1
+ """Detector for package.json files (npm/Node.js dependencies and scripts)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
8
+ from osscodeiq.models.graph import (
9
+ EdgeKind,
10
+ GraphEdge,
11
+ GraphNode,
12
+ NodeKind,
13
+ SourceLocation,
14
+ )
15
+
16
+
17
+ class PackageJsonDetector:
18
+ """Detects module dependencies and scripts from package.json files."""
19
+
20
+ name: str = "package_json"
21
+ supported_languages: tuple[str, ...] = ("json",)
22
+
23
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
24
+ result = DetectorResult()
25
+
26
+ # Only trigger for files named exactly "package.json"
27
+ if os.path.basename(ctx.file_path) != "package.json":
28
+ return result
29
+
30
+ data = ctx.parsed_data
31
+ if not isinstance(data, dict) or not isinstance(data.get("data"), dict):
32
+ return result
33
+
34
+ pkg = data["data"]
35
+ filepath = ctx.file_path
36
+ module_id = f"npm:{filepath}"
37
+ pkg_name = pkg.get("name") or filepath
38
+
39
+ # MODULE node for the package
40
+ props: dict[str, object] = {"package_name": pkg_name}
41
+ version = pkg.get("version")
42
+ if version:
43
+ props["version"] = version
44
+
45
+ result.nodes.append(GraphNode(
46
+ id=module_id,
47
+ kind=NodeKind.MODULE,
48
+ label=pkg_name,
49
+ fqn=pkg_name,
50
+ module=ctx.module_name,
51
+ location=SourceLocation(file_path=filepath),
52
+ properties=props,
53
+ ))
54
+
55
+ # DEPENDS_ON edges for dependencies and devDependencies
56
+ for dep_key in ("dependencies", "devDependencies"):
57
+ deps = pkg.get(dep_key)
58
+ if not isinstance(deps, dict):
59
+ continue
60
+ for dep_name, dep_version in deps.items():
61
+ if not isinstance(dep_name, str):
62
+ continue
63
+ edge_props: dict[str, object] = {"dep_type": dep_key}
64
+ if isinstance(dep_version, str):
65
+ edge_props["version_spec"] = dep_version
66
+ result.edges.append(GraphEdge(
67
+ source=module_id,
68
+ target=f"npm:{dep_name}",
69
+ kind=EdgeKind.DEPENDS_ON,
70
+ label=f"{pkg_name} depends on {dep_name}",
71
+ properties=edge_props,
72
+ ))
73
+
74
+ # METHOD nodes for each script
75
+ scripts = pkg.get("scripts")
76
+ if isinstance(scripts, dict):
77
+ for script_name, script_cmd in scripts.items():
78
+ if not isinstance(script_name, str):
79
+ continue
80
+ script_id = f"npm:{filepath}:script:{script_name}"
81
+ script_props: dict[str, object] = {"script_name": script_name}
82
+ if isinstance(script_cmd, str):
83
+ script_props["command"] = script_cmd
84
+ result.nodes.append(GraphNode(
85
+ id=script_id,
86
+ kind=NodeKind.METHOD,
87
+ label=f"npm run {script_name}",
88
+ module=ctx.module_name,
89
+ location=SourceLocation(file_path=filepath),
90
+ properties=script_props,
91
+ ))
92
+ result.edges.append(GraphEdge(
93
+ source=module_id,
94
+ target=script_id,
95
+ kind=EdgeKind.CONTAINS,
96
+ label=f"{pkg_name} contains script {script_name}",
97
+ ))
98
+
99
+ return result
@@ -0,0 +1,108 @@
1
+ """Properties file detector for Java .properties and Spring configuration files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
6
+ from osscodeiq.models.graph import (
7
+ EdgeKind,
8
+ GraphEdge,
9
+ GraphNode,
10
+ NodeKind,
11
+ SourceLocation,
12
+ )
13
+
14
+ _DB_KEYWORDS = {"url", "jdbc", "datasource"}
15
+ _MAX_KEYS = 200
16
+
17
+
18
+ class PropertiesDetector:
19
+ """Detects property keys, Spring config markers, and database connections from .properties files."""
20
+
21
+ name: str = "properties"
22
+ supported_languages: tuple[str, ...] = ("properties",)
23
+
24
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
25
+ result = DetectorResult()
26
+
27
+ # parsed_data expected: {"type": "properties", "file": path, "data": {"key": "value", ...}}
28
+ if not isinstance(ctx.parsed_data, dict):
29
+ return result
30
+
31
+ if ctx.parsed_data.get("type") != "properties":
32
+ return result
33
+
34
+ data = ctx.parsed_data.get("data")
35
+ if not isinstance(data, dict):
36
+ return result
37
+
38
+ filepath = ctx.file_path
39
+ file_id = f"props:{filepath}"
40
+
41
+ # CONFIG_FILE node
42
+ result.nodes.append(GraphNode(
43
+ id=file_id,
44
+ kind=NodeKind.CONFIG_FILE,
45
+ label=filepath,
46
+ fqn=filepath,
47
+ module=ctx.module_name,
48
+ location=SourceLocation(
49
+ file_path=filepath,
50
+ line_start=1,
51
+ ),
52
+ properties={"format": "properties"},
53
+ ))
54
+
55
+ # Process keys (limit to avoid node explosion)
56
+ keys = list(data.items())[:_MAX_KEYS]
57
+
58
+ for key, value in keys:
59
+ if not isinstance(key, str):
60
+ continue
61
+
62
+ key_lower = key.lower()
63
+ key_id = f"props:{filepath}:{key}"
64
+
65
+ # Detect DB connection properties
66
+ is_db = any(kw in key_lower for kw in _DB_KEYWORDS)
67
+
68
+ if is_db:
69
+ props: dict[str, object] = {"key": key}
70
+ if isinstance(value, str):
71
+ props["value"] = value
72
+
73
+ result.nodes.append(GraphNode(
74
+ id=key_id,
75
+ kind=NodeKind.DATABASE_CONNECTION,
76
+ label=key,
77
+ fqn=f"{filepath}:{key}",
78
+ module=ctx.module_name,
79
+ location=SourceLocation(file_path=filepath),
80
+ properties=props,
81
+ ))
82
+ else:
83
+ props = {"key": key}
84
+ if isinstance(value, str):
85
+ props["value"] = value
86
+
87
+ # Detect Spring config
88
+ if key.startswith("spring."):
89
+ props["spring_config"] = True
90
+
91
+ result.nodes.append(GraphNode(
92
+ id=key_id,
93
+ kind=NodeKind.CONFIG_KEY,
94
+ label=key,
95
+ fqn=f"{filepath}:{key}",
96
+ module=ctx.module_name,
97
+ location=SourceLocation(file_path=filepath),
98
+ properties=props,
99
+ ))
100
+
101
+ result.edges.append(GraphEdge(
102
+ source=file_id,
103
+ target=key_id,
104
+ kind=EdgeKind.CONTAINS,
105
+ label=f"{filepath} contains {key}",
106
+ ))
107
+
108
+ return result
@@ -0,0 +1,169 @@
1
+ """Detector for pyproject.toml files (Python project metadata and dependencies)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+
8
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
9
+ from osscodeiq.detectors.utils import decode_text
10
+ from osscodeiq.models.graph import (
11
+ EdgeKind,
12
+ GraphEdge,
13
+ GraphNode,
14
+ NodeKind,
15
+ SourceLocation,
16
+ )
17
+
18
+ if sys.version_info >= (3, 11):
19
+ import tomllib
20
+ else:
21
+ try:
22
+ import tomli as tomllib # type: ignore[no-redef]
23
+ except ImportError:
24
+ tomllib = None # type: ignore[assignment]
25
+
26
+
27
+ class PyprojectTomlDetector:
28
+ """Detects Python project metadata, dependencies, and entry points from pyproject.toml."""
29
+
30
+ name: str = "pyproject_toml"
31
+ supported_languages: tuple[str, ...] = ("toml",)
32
+
33
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
34
+ result = DetectorResult()
35
+
36
+ # Only trigger for files named exactly "pyproject.toml"
37
+ if os.path.basename(ctx.file_path) != "pyproject.toml":
38
+ return result
39
+
40
+ if tomllib is None:
41
+ return result
42
+
43
+ try:
44
+ data = tomllib.loads(decode_text(ctx))
45
+ except Exception:
46
+ return result
47
+
48
+ if not isinstance(data, dict):
49
+ return result
50
+
51
+ filepath = ctx.file_path
52
+ module_id = f"pypi:{filepath}"
53
+
54
+ # Resolve project name from [project] or [tool.poetry]
55
+ project_section = data.get("project", {})
56
+ poetry_section = data.get("tool", {}).get("poetry", {})
57
+
58
+ pkg_name = (
59
+ project_section.get("name")
60
+ or poetry_section.get("name")
61
+ or filepath
62
+ )
63
+
64
+ # Module properties
65
+ props: dict[str, object] = {"package_name": pkg_name}
66
+ version = project_section.get("version") or poetry_section.get("version")
67
+ if version:
68
+ props["version"] = version
69
+ description = project_section.get("description") or poetry_section.get("description")
70
+ if description:
71
+ props["description"] = description
72
+
73
+ # MODULE node for the project
74
+ result.nodes.append(GraphNode(
75
+ id=module_id,
76
+ kind=NodeKind.MODULE,
77
+ label=pkg_name,
78
+ fqn=pkg_name,
79
+ module=ctx.module_name,
80
+ location=SourceLocation(file_path=filepath),
81
+ properties=props,
82
+ ))
83
+
84
+ # DEPENDS_ON edges for dependencies
85
+ # PEP 621 style: [project].dependencies is a list of strings
86
+ pep621_deps = project_section.get("dependencies", [])
87
+ if isinstance(pep621_deps, list):
88
+ for dep_spec in pep621_deps:
89
+ if not isinstance(dep_spec, str):
90
+ continue
91
+ # Extract package name from spec like "requests>=2.0"
92
+ dep_name = _parse_dep_name(dep_spec)
93
+ if dep_name:
94
+ result.edges.append(GraphEdge(
95
+ source=module_id,
96
+ target=f"pypi:{dep_name}",
97
+ kind=EdgeKind.DEPENDS_ON,
98
+ label=f"{pkg_name} depends on {dep_name}",
99
+ properties={"dep_spec": dep_spec},
100
+ ))
101
+
102
+ # Poetry style: [tool.poetry].dependencies is a dict
103
+ poetry_deps = poetry_section.get("dependencies", {})
104
+ if isinstance(poetry_deps, dict):
105
+ for dep_name, dep_version in poetry_deps.items():
106
+ if not isinstance(dep_name, str):
107
+ continue
108
+ # Skip python itself
109
+ if dep_name.lower() == "python":
110
+ continue
111
+ edge_props: dict[str, object] = {}
112
+ if isinstance(dep_version, str):
113
+ edge_props["version_spec"] = dep_version
114
+ result.edges.append(GraphEdge(
115
+ source=module_id,
116
+ target=f"pypi:{dep_name}",
117
+ kind=EdgeKind.DEPENDS_ON,
118
+ label=f"{pkg_name} depends on {dep_name}",
119
+ properties=edge_props,
120
+ ))
121
+
122
+ # CONFIG_DEFINITION nodes for entry points / scripts
123
+ scripts = project_section.get("scripts", {})
124
+ if not isinstance(scripts, dict):
125
+ scripts = {}
126
+ poetry_scripts = poetry_section.get("scripts", {})
127
+ if isinstance(poetry_scripts, dict):
128
+ scripts.update(poetry_scripts)
129
+
130
+ for script_name, script_target in scripts.items():
131
+ if not isinstance(script_name, str):
132
+ continue
133
+ script_id = f"pypi:{filepath}:script:{script_name}"
134
+ script_props: dict[str, object] = {"script_name": script_name}
135
+ if isinstance(script_target, str):
136
+ script_props["target"] = script_target
137
+
138
+ result.nodes.append(GraphNode(
139
+ id=script_id,
140
+ kind=NodeKind.CONFIG_DEFINITION,
141
+ label=script_name,
142
+ fqn=f"{pkg_name}:script:{script_name}",
143
+ module=ctx.module_name,
144
+ location=SourceLocation(file_path=filepath),
145
+ properties=script_props,
146
+ ))
147
+
148
+ result.edges.append(GraphEdge(
149
+ source=module_id,
150
+ target=script_id,
151
+ kind=EdgeKind.CONTAINS,
152
+ label=f"{pkg_name} defines script {script_name}",
153
+ ))
154
+
155
+ return result
156
+
157
+
158
+ def _parse_dep_name(spec: str) -> str | None:
159
+ """Extract package name from a PEP 508 dependency specifier."""
160
+ # e.g. "requests>=2.0", "numpy", "black[jupyter]>=22.0"
161
+ spec = spec.strip()
162
+ if not spec:
163
+ return None
164
+ # Split on version specifiers or extras
165
+ for ch in ">=<![;@ ":
166
+ idx = spec.find(ch)
167
+ if idx > 0:
168
+ spec = spec[:idx]
169
+ return spec.strip() or None