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,115 @@
1
+ """Pydantic model detector."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
8
+ from osscodeiq.detectors.utils import decode_text
9
+ from osscodeiq.models.graph import EdgeKind, GraphEdge, GraphNode, NodeKind, SourceLocation
10
+
11
+
12
+ class PydanticModelDetector:
13
+ """Detects Pydantic model and settings definitions."""
14
+
15
+ name: str = "python.pydantic_models"
16
+ supported_languages: tuple[str, ...] = ("python",)
17
+
18
+ _PYDANTIC_CLASS_RE = re.compile(
19
+ r"^class\s+(\w+)\s*\(\s*(\w*(?:BaseModel|BaseSettings)\w*)\s*\)", re.MULTILINE
20
+ )
21
+ _FIELD_RE = re.compile(r"^\s+(\w+)\s*:\s*(\w[\w\[\], |]*)", re.MULTILINE)
22
+ _VALIDATOR_RE = re.compile(
23
+ r"@(?:validator|field_validator)\s*\(\s*[\"'](\w+)", re.MULTILINE
24
+ )
25
+ _CONFIG_CLASS_RE = re.compile(r"^\s+class\s+Config\s*:", re.MULTILINE)
26
+ _CONFIG_ATTR_RE = re.compile(r"^\s{8}(\w+)\s*=\s*(.+)", re.MULTILINE)
27
+
28
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
29
+ result = DetectorResult()
30
+ text = decode_text(ctx)
31
+
32
+ # Track known model names for inheritance edges
33
+ known_models: dict[str, str] = {}
34
+
35
+ for match in self._PYDANTIC_CLASS_RE.finditer(text):
36
+ class_name = match.group(1)
37
+ base_class = match.group(2)
38
+ line = text[: match.start()].count("\n") + 1
39
+
40
+ is_settings = "BaseSettings" in base_class
41
+
42
+ # Determine class body boundaries
43
+ class_start = match.start()
44
+ next_class = re.search(r"\nclass\s+\w+", text[match.end() :])
45
+ class_body = (
46
+ text[class_start : match.end() + next_class.start()]
47
+ if next_class
48
+ else text[class_start:]
49
+ )
50
+
51
+ # Extract fields
52
+ fields = []
53
+ field_types: dict[str, str] = {}
54
+ for fm in self._FIELD_RE.finditer(class_body):
55
+ fname = fm.group(1)
56
+ ftype = fm.group(2).strip()
57
+ if fname not in ("class", "Config", "model_config"):
58
+ fields.append(fname)
59
+ field_types[fname] = ftype
60
+
61
+ # Extract validators
62
+ validators = [
63
+ vm.group(1) for vm in self._VALIDATOR_RE.finditer(class_body)
64
+ ]
65
+
66
+ # Extract Config class properties
67
+ config_props: dict[str, str] = {}
68
+ config_match = self._CONFIG_CLASS_RE.search(class_body)
69
+ if config_match:
70
+ config_block_start = config_match.end()
71
+ # Find next dedented line or end
72
+ config_block_end = len(class_body)
73
+ for cm in re.finditer(r"\n\S", class_body[config_block_start:]):
74
+ config_block_end = config_block_start + cm.start()
75
+ break
76
+ config_block = class_body[config_block_start:config_block_end]
77
+ for attr_match in self._CONFIG_ATTR_RE.finditer(config_block):
78
+ config_props[attr_match.group(1)] = attr_match.group(2).strip()
79
+
80
+ node_kind = NodeKind.CONFIG_DEFINITION if is_settings else NodeKind.ENTITY
81
+ node_id = f"pydantic:{ctx.file_path}:model:{class_name}"
82
+
83
+ result.nodes.append(
84
+ GraphNode(
85
+ id=node_id,
86
+ kind=node_kind,
87
+ label=class_name,
88
+ fqn=f"{ctx.file_path}::{class_name}",
89
+ module=ctx.module_name,
90
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
91
+ annotations=validators,
92
+ properties={
93
+ "fields": fields,
94
+ "field_types": field_types,
95
+ "framework": "pydantic",
96
+ "base_class": base_class,
97
+ **({"config": config_props} if config_props else {}),
98
+ },
99
+ )
100
+ )
101
+
102
+ known_models[class_name] = node_id
103
+
104
+ # Check for inheritance from a known model
105
+ if base_class in known_models:
106
+ result.edges.append(
107
+ GraphEdge(
108
+ source=node_id,
109
+ target=known_models[base_class],
110
+ kind=EdgeKind.EXTENDS,
111
+ label=f"{class_name} extends {base_class}",
112
+ )
113
+ )
114
+
115
+ return result
@@ -0,0 +1,234 @@
1
+ """Regex-based Python structures detector for Python source files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
8
+ from osscodeiq.detectors.utils import decode_text, find_line_number
9
+ from osscodeiq.models.graph import (
10
+ EdgeKind,
11
+ GraphEdge,
12
+ GraphNode,
13
+ NodeKind,
14
+ SourceLocation,
15
+ )
16
+
17
+ _CLASS_RE = re.compile(r'^class\s+(\w+)(?:\(([^)]*)\))?:', re.MULTILINE)
18
+ _FUNC_RE = re.compile(r'^([^\S\n]*)(async\s+)?def\s+(\w+)\s*\(', re.MULTILINE)
19
+ _IMPORT_RE = re.compile(r'^(?:from\s+([\w.]+)\s+)?import\s+([\w., ]+)', re.MULTILINE)
20
+ _DECORATOR_RE = re.compile(r'^([^\S\n]*)@(\w[\w.]*)', re.MULTILINE)
21
+ _ALL_RE = re.compile(r'__all__\s*=\s*\[([^\]]*)\]', re.DOTALL)
22
+
23
+
24
+ def _collect_decorators(text: str) -> dict[int, list[str]]:
25
+ """Map each decorator's line number to the decorator name.
26
+
27
+ Returns a dict keyed by 1-based line number of the decorator.
28
+ """
29
+ result: dict[int, list[str]] = {}
30
+ for m in _DECORATOR_RE.finditer(text):
31
+ line = find_line_number(text, m.start())
32
+ result.setdefault(line, []).append(m.group(2))
33
+ return result
34
+
35
+
36
+ def _find_decorators_for_line(
37
+ decorator_map: dict[int, list[str]], target_line: int
38
+ ) -> list[str]:
39
+ """Collect all decorator names immediately above a target line."""
40
+ decorators: list[str] = []
41
+ line = target_line - 1
42
+ while line in decorator_map:
43
+ decorators.extend(decorator_map[line])
44
+ line -= 1
45
+ # Reverse so top-most decorator is first
46
+ decorators.reverse()
47
+ return decorators
48
+
49
+
50
+ def _find_enclosing_class(
51
+ class_ranges: list[tuple[str, int, int]], line: int, func_indent: int
52
+ ) -> str | None:
53
+ """Find the class name that encloses a given line number.
54
+
55
+ A function is considered inside a class if it appears after the class
56
+ declaration and is indented more than the class itself.
57
+
58
+ class_ranges is a list of (class_name, start_line, indent_col).
59
+ """
60
+ for class_name, start_line, class_indent in reversed(class_ranges):
61
+ if line > start_line and func_indent > class_indent:
62
+ return class_name
63
+ return None
64
+
65
+
66
+ class PythonStructuresDetector:
67
+ """Detects Python classes, functions, imports, decorators, and __all__ exports."""
68
+
69
+ name: str = "python_structures"
70
+ supported_languages: tuple[str, ...] = ("python",)
71
+
72
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
73
+ result = DetectorResult()
74
+ text = decode_text(ctx)
75
+ fp = ctx.file_path
76
+ file_node_id = fp
77
+
78
+ # Collect decorators by line number
79
+ decorator_map = _collect_decorators(text)
80
+
81
+ # __all__ exports
82
+ all_match = _ALL_RE.search(text)
83
+ all_exports: list[str] | None = None
84
+ if all_match:
85
+ raw = all_match.group(1)
86
+ # Extract quoted names
87
+ all_exports = re.findall(r"""['"](\w+)['"]""", raw)
88
+
89
+ # Classes — track them for method association
90
+ class_ranges: list[tuple[str, int, int]] = [] # (name, line, indent_col)
91
+ for m in _CLASS_RE.finditer(text):
92
+ class_name = m.group(1)
93
+ bases_str = m.group(2)
94
+ line = find_line_number(text, m.start())
95
+ node_id = f"py:{fp}:class:{class_name}"
96
+
97
+ # Find indent of the class declaration
98
+ line_start_offset = text.rfind("\n", 0, m.start()) + 1
99
+ indent = m.start() - line_start_offset
100
+
101
+ class_ranges.append((class_name, line, indent))
102
+
103
+ annotations = _find_decorators_for_line(decorator_map, line)
104
+
105
+ properties: dict = {}
106
+ if bases_str:
107
+ bases = [b.strip() for b in bases_str.split(",") if b.strip()]
108
+ properties["bases"] = bases
109
+ if all_exports and class_name in all_exports:
110
+ properties["exported"] = True
111
+
112
+ result.nodes.append(GraphNode(
113
+ id=node_id,
114
+ kind=NodeKind.CLASS,
115
+ label=class_name,
116
+ fqn=class_name,
117
+ module=ctx.module_name,
118
+ location=SourceLocation(
119
+ file_path=fp,
120
+ line_start=line,
121
+ ),
122
+ annotations=annotations,
123
+ properties=properties,
124
+ ))
125
+
126
+ # EXTENDS edges for base classes
127
+ if bases_str:
128
+ bases = [b.strip() for b in bases_str.split(",") if b.strip()]
129
+ for base in bases:
130
+ result.edges.append(GraphEdge(
131
+ source=node_id,
132
+ target=base,
133
+ kind=EdgeKind.EXTENDS,
134
+ label=f"{class_name} extends {base}",
135
+ ))
136
+
137
+ # Functions and methods
138
+ for m in _FUNC_RE.finditer(text):
139
+ indent_str = m.group(1)
140
+ is_async = m.group(2) is not None
141
+ func_name = m.group(3)
142
+ line = find_line_number(text, m.start())
143
+ indent_len = len(indent_str)
144
+
145
+ annotations = _find_decorators_for_line(decorator_map, line)
146
+
147
+ properties: dict = {}
148
+ if is_async:
149
+ properties["async"] = True
150
+ if all_exports and func_name in all_exports:
151
+ properties["exported"] = True
152
+
153
+ if indent_len == 0:
154
+ # Top-level function
155
+ node_id = f"py:{fp}:func:{func_name}"
156
+ result.nodes.append(GraphNode(
157
+ id=node_id,
158
+ kind=NodeKind.METHOD,
159
+ label=func_name,
160
+ fqn=func_name,
161
+ module=ctx.module_name,
162
+ location=SourceLocation(
163
+ file_path=fp,
164
+ line_start=line,
165
+ ),
166
+ annotations=annotations,
167
+ properties=properties,
168
+ ))
169
+ else:
170
+ # Indented def — check if it's inside a class
171
+ enclosing_class = _find_enclosing_class(class_ranges, line, indent_len)
172
+ if enclosing_class:
173
+ node_id = f"py:{fp}:class:{enclosing_class}:method:{func_name}"
174
+ properties["class"] = enclosing_class
175
+ result.nodes.append(GraphNode(
176
+ id=node_id,
177
+ kind=NodeKind.METHOD,
178
+ label=f"{enclosing_class}.{func_name}",
179
+ fqn=f"{enclosing_class}.{func_name}",
180
+ module=ctx.module_name,
181
+ location=SourceLocation(
182
+ file_path=fp,
183
+ line_start=line,
184
+ ),
185
+ annotations=annotations,
186
+ properties=properties,
187
+ ))
188
+ # DEFINES edge from class to method
189
+ class_node_id = f"py:{fp}:class:{enclosing_class}"
190
+ result.edges.append(GraphEdge(
191
+ source=class_node_id,
192
+ target=node_id,
193
+ kind=EdgeKind.DEFINES,
194
+ label=f"{enclosing_class} defines {func_name}",
195
+ ))
196
+
197
+ # Imports
198
+ for m in _IMPORT_RE.finditer(text):
199
+ from_module = m.group(1)
200
+ import_names = m.group(2)
201
+ if from_module:
202
+ result.edges.append(GraphEdge(
203
+ source=file_node_id,
204
+ target=from_module,
205
+ kind=EdgeKind.IMPORTS,
206
+ label=f"{fp} imports {from_module}",
207
+ ))
208
+ else:
209
+ for name in import_names.split(","):
210
+ name = name.strip()
211
+ if name:
212
+ result.edges.append(GraphEdge(
213
+ source=file_node_id,
214
+ target=name,
215
+ kind=EdgeKind.IMPORTS,
216
+ label=f"{fp} imports {name}",
217
+ ))
218
+
219
+ # __all__ property on a file-level module node (only if present)
220
+ if all_exports is not None:
221
+ result.nodes.append(GraphNode(
222
+ id=f"py:{fp}:module",
223
+ kind=NodeKind.MODULE,
224
+ label=fp,
225
+ fqn=fp,
226
+ module=ctx.module_name,
227
+ location=SourceLocation(
228
+ file_path=fp,
229
+ line_start=find_line_number(text, all_match.start()),
230
+ ),
231
+ properties={"__all__": all_exports},
232
+ ))
233
+
234
+ return result
@@ -0,0 +1,82 @@
1
+ """SQLAlchemy model detector."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
8
+ from osscodeiq.detectors.utils import decode_text
9
+ from osscodeiq.models.graph import EdgeKind, GraphEdge, GraphNode, NodeKind, SourceLocation
10
+
11
+
12
+ class SQLAlchemyModelDetector:
13
+ """Detects SQLAlchemy ORM models (declarative base classes)."""
14
+
15
+ name: str = "python.sqlalchemy_models"
16
+ supported_languages: tuple[str, ...] = ("python",)
17
+
18
+ # class User(Base): or class User(db.Model):
19
+ _MODEL_PATTERN = re.compile(
20
+ r"class\s+(\w+)\(([^)]*(?:Base|Model|DeclarativeBase)[^)]*)\):"
21
+ )
22
+
23
+ # __tablename__ = 'users'
24
+ _TABLE_NAME = re.compile(r"__tablename__\s*=\s*['\"](\w+)['\"]")
25
+
26
+ # Column definitions: name = Column(String, ...) or name: Mapped[str] = mapped_column(...)
27
+ _COLUMN_PATTERN = re.compile(
28
+ r"(\w+)\s*(?::\s*Mapped\[.*?\])?\s*=\s*(?:Column|mapped_column)\("
29
+ )
30
+
31
+ # Relationship: orders = relationship("Order", ...) or orders: Mapped[list["Order"]]
32
+ _RELATIONSHIP_PATTERN = re.compile(
33
+ r"(\w+)\s*(?::\s*Mapped\[.*?\])?\s*=\s*relationship\(\s*['\"](\w+)['\"]"
34
+ )
35
+
36
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
37
+ result = DetectorResult()
38
+ text = decode_text(ctx)
39
+
40
+ for match in self._MODEL_PATTERN.finditer(text):
41
+ class_name = match.group(1)
42
+ line = text[:match.start()].count("\n") + 1
43
+
44
+ # Find the class body (rough: from class line to next class or end)
45
+ class_start = match.start()
46
+ next_class = re.search(r"\nclass\s+\w+", text[match.end():])
47
+ class_body = text[class_start:match.end() + next_class.start()] if next_class else text[class_start:]
48
+
49
+ # Extract table name
50
+ table_match = self._TABLE_NAME.search(class_body)
51
+ table_name = table_match.group(1) if table_match else class_name.lower() + "s"
52
+
53
+ # Extract columns
54
+ columns = [m.group(1) for m in self._COLUMN_PATTERN.finditer(class_body)]
55
+
56
+ node_id = f"entity:{ctx.module_name or ''}:{class_name}"
57
+ result.nodes.append(GraphNode(
58
+ id=node_id,
59
+ kind=NodeKind.ENTITY,
60
+ label=class_name,
61
+ fqn=f"{ctx.file_path}::{class_name}",
62
+ module=ctx.module_name,
63
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
64
+ properties={
65
+ "table_name": table_name,
66
+ "columns": columns,
67
+ "framework": "sqlalchemy",
68
+ },
69
+ ))
70
+
71
+ # Extract relationships
72
+ for rel_match in self._RELATIONSHIP_PATTERN.finditer(class_body):
73
+ target_class = rel_match.group(2)
74
+ target_id = f"entity:{ctx.module_name or ''}:{target_class}"
75
+ result.edges.append(GraphEdge(
76
+ source=node_id,
77
+ target=target_id,
78
+ kind=EdgeKind.MAPS_TO,
79
+ label=rel_match.group(1),
80
+ ))
81
+
82
+ return result
@@ -0,0 +1,100 @@
1
+ """Plugin registry for OSSCodeIQ detectors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import importlib.metadata
7
+ import logging
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from osscodeiq.detectors.base import Detector
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class DetectorRegistry:
17
+ """Registry that manages detector instances and plugin discovery."""
18
+
19
+ def __init__(self) -> None:
20
+ self._detectors: list[Detector] = []
21
+ self._by_name: dict[str, Detector] = {}
22
+
23
+ def register(self, detector: Detector) -> None:
24
+ """Add a detector to the registry."""
25
+ if detector.name in self._by_name:
26
+ logger.warning("Detector %r already registered, skipping", detector.name)
27
+ return
28
+ self._detectors.append(detector)
29
+ self._by_name[detector.name] = detector
30
+
31
+ def load_builtin_detectors(self) -> None:
32
+ """Import and register all built-in detectors via package scanning."""
33
+ import pkgutil
34
+
35
+ import osscodeiq.detectors as detectors_pkg
36
+
37
+ # Walk all subpackages under osscodeiq.detectors
38
+ skip = {"registry", "base", "utils", "__init__"}
39
+ module_paths = []
40
+ for importer, modname, ispkg in pkgutil.walk_packages(
41
+ detectors_pkg.__path__,
42
+ prefix="osscodeiq.detectors.",
43
+ ):
44
+ # Skip non-detector modules
45
+ short_name = modname.rsplit(".", 1)[-1]
46
+ if short_name in skip or short_name.startswith("_"):
47
+ continue
48
+ module_paths.append(modname)
49
+
50
+ # Sort for deterministic registration order
51
+ module_paths.sort()
52
+
53
+ for module_path in module_paths:
54
+ try:
55
+ mod = importlib.import_module(module_path)
56
+ # Convention: each module exposes a `detector` attribute or a class
57
+ # ending with "Detector"
58
+ if hasattr(mod, "detector"):
59
+ self.register(mod.detector)
60
+ else:
61
+ for attr_name in dir(mod):
62
+ obj = getattr(mod, attr_name)
63
+ if (
64
+ isinstance(obj, type)
65
+ and attr_name.endswith("Detector")
66
+ and hasattr(obj, "detect")
67
+ ):
68
+ self.register(obj())
69
+ break
70
+ except Exception:
71
+ logger.debug("Could not load detector module %s", module_path, exc_info=True)
72
+
73
+ def load_plugin_detectors(self) -> None:
74
+ """Discover detectors via setuptools entry points."""
75
+ try:
76
+ eps = importlib.metadata.entry_points(group="osscodeiq.detectors")
77
+ except TypeError:
78
+ # Python < 3.12 compat
79
+ eps = importlib.metadata.entry_points().get("osscodeiq.detectors", [])
80
+ for ep in eps:
81
+ try:
82
+ detector_or_factory = ep.load()
83
+ if callable(detector_or_factory) and isinstance(detector_or_factory, type):
84
+ self.register(detector_or_factory())
85
+ else:
86
+ self.register(detector_or_factory)
87
+ except Exception:
88
+ logger.warning("Failed to load plugin detector %s", ep.name, exc_info=True)
89
+
90
+ def detectors_for_language(self, language: str) -> list[Detector]:
91
+ """Return all detectors that support a given language."""
92
+ return [d for d in self._detectors if language in d.supported_languages]
93
+
94
+ def all_detectors(self) -> list[Detector]:
95
+ """Return all registered detectors."""
96
+ return list(self._detectors)
97
+
98
+ def get(self, name: str) -> Detector | None:
99
+ """Get a detector by name."""
100
+ return self._by_name.get(name)
File without changes