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
osscodeiq/output/dot.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Graphviz DOT renderer for the OSSCodeIQ graph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from osscodeiq.graph.store import GraphStore
|
|
9
|
+
from osscodeiq.models.graph import EdgeKind, GraphEdge, GraphNode, NodeKind
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _sanitize_id(raw: str) -> str:
|
|
13
|
+
"""Replace characters invalid in DOT identifiers."""
|
|
14
|
+
return re.sub(r"[^a-zA-Z0-9_]", "_", raw)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _quote(text: str) -> str:
|
|
18
|
+
"""Escape a string for use inside DOT double-quotes."""
|
|
19
|
+
return text.replace("\\", "\\\\").replace('"', '\\"')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# -- Node shape and colour mapping -----------------------------------------
|
|
23
|
+
|
|
24
|
+
_NODE_STYLES: dict[NodeKind, dict[str, str]] = {
|
|
25
|
+
NodeKind.MODULE: {"shape": "box3d", "fillcolor": "#4A90D9", "fontcolor": "white"},
|
|
26
|
+
NodeKind.PACKAGE: {"shape": "box3d", "fillcolor": "#4A90D9", "fontcolor": "white"},
|
|
27
|
+
NodeKind.CLASS: {"shape": "box", "fillcolor": "#A8D8EA", "fontcolor": "black"},
|
|
28
|
+
NodeKind.METHOD: {"shape": "box", "fillcolor": "#D4E6F1", "fontcolor": "black"},
|
|
29
|
+
NodeKind.ENDPOINT: {"shape": "hexagon", "fillcolor": "#F9E79F", "fontcolor": "black"},
|
|
30
|
+
NodeKind.ENTITY: {"shape": "cylinder", "fillcolor": "#ABEBC6", "fontcolor": "black"},
|
|
31
|
+
NodeKind.REPOSITORY: {"shape": "cylinder", "fillcolor": "#ABEBC6", "fontcolor": "black"},
|
|
32
|
+
NodeKind.QUERY: {"shape": "box", "fillcolor": "#D5F5E3", "fontcolor": "black"},
|
|
33
|
+
NodeKind.MIGRATION: {"shape": "box", "fillcolor": "#D5F5E3", "fontcolor": "black"},
|
|
34
|
+
NodeKind.TOPIC: {"shape": "parallelogram", "fillcolor": "#F5B7B1", "fontcolor": "black"},
|
|
35
|
+
NodeKind.QUEUE: {"shape": "parallelogram", "fillcolor": "#F5B7B1", "fontcolor": "black"},
|
|
36
|
+
NodeKind.EVENT: {"shape": "parallelogram", "fillcolor": "#F5B7B1", "fontcolor": "black"},
|
|
37
|
+
NodeKind.RMI_INTERFACE: {"shape": "component", "fillcolor": "#D7BDE2", "fontcolor": "black"},
|
|
38
|
+
NodeKind.CONFIG_FILE: {"shape": "note", "fillcolor": "#FDEBD0", "fontcolor": "black"},
|
|
39
|
+
NodeKind.CONFIG_KEY: {"shape": "note", "fillcolor": "#FDEBD0", "fontcolor": "black"},
|
|
40
|
+
NodeKind.WEBSOCKET_ENDPOINT: {"shape": "hexagon", "fillcolor": "#F9E79F", "fontcolor": "black"},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# -- Edge styles -----------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
_EDGE_STYLES: dict[EdgeKind, dict[str, str]] = {
|
|
46
|
+
EdgeKind.CALLS: {"style": "solid", "arrowhead": "normal"},
|
|
47
|
+
EdgeKind.DEPENDS_ON: {"style": "solid", "arrowhead": "normal"},
|
|
48
|
+
EdgeKind.IMPORTS: {"style": "solid", "arrowhead": "normal"},
|
|
49
|
+
EdgeKind.INJECTS: {"style": "solid", "arrowhead": "normal"},
|
|
50
|
+
EdgeKind.QUERIES: {"style": "solid", "arrowhead": "normal"},
|
|
51
|
+
EdgeKind.MAPS_TO: {"style": "solid", "arrowhead": "normal"},
|
|
52
|
+
EdgeKind.READS_CONFIG: {"style": "solid", "arrowhead": "normal"},
|
|
53
|
+
EdgeKind.MIGRATES: {"style": "solid", "arrowhead": "normal"},
|
|
54
|
+
EdgeKind.CONTAINS: {"style": "solid", "arrowhead": "normal"},
|
|
55
|
+
EdgeKind.EXPOSES: {"style": "solid", "arrowhead": "normal"},
|
|
56
|
+
EdgeKind.PRODUCES: {"style": "dashed", "arrowhead": "normal"},
|
|
57
|
+
EdgeKind.CONSUMES: {"style": "dashed", "arrowhead": "normal"},
|
|
58
|
+
EdgeKind.PUBLISHES: {"style": "dashed", "arrowhead": "normal"},
|
|
59
|
+
EdgeKind.LISTENS: {"style": "dashed", "arrowhead": "normal"},
|
|
60
|
+
EdgeKind.INVOKES_RMI: {"style": "dashed", "arrowhead": "normal"},
|
|
61
|
+
EdgeKind.EXPORTS_RMI: {"style": "dashed", "arrowhead": "normal"},
|
|
62
|
+
EdgeKind.EXTENDS: {"style": "solid", "arrowhead": "empty"},
|
|
63
|
+
EdgeKind.IMPLEMENTS: {"style": "solid", "arrowhead": "empty"},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
ClusterBy = Literal["module", "domain", "node-type", None]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DotRenderer:
|
|
71
|
+
"""Render a :class:`GraphStore` as a Graphviz DOT graph."""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
rankdir: str = "LR",
|
|
76
|
+
cluster_by: ClusterBy = None,
|
|
77
|
+
fontname: str = "Helvetica",
|
|
78
|
+
fontsize: str = "11",
|
|
79
|
+
) -> None:
|
|
80
|
+
self._rankdir = rankdir
|
|
81
|
+
self._cluster_by = cluster_by
|
|
82
|
+
self._fontname = fontname
|
|
83
|
+
self._fontsize = fontsize
|
|
84
|
+
|
|
85
|
+
# -- public API -------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def render(self, store: GraphStore, cluster_by: ClusterBy | None = None) -> str:
|
|
88
|
+
"""Return a DOT-language string."""
|
|
89
|
+
lines: list[str] = [
|
|
90
|
+
"digraph CodeIntelligence {",
|
|
91
|
+
f' rankdir={self._rankdir};',
|
|
92
|
+
f' fontname="{self._fontname}";',
|
|
93
|
+
f' fontsize={self._fontsize};',
|
|
94
|
+
f' node [fontname="{self._fontname}", fontsize={self._fontsize}, style=filled];',
|
|
95
|
+
f' edge [fontname="{self._fontname}", fontsize={self._fontsize}];',
|
|
96
|
+
"",
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
nodes = store.all_nodes()
|
|
100
|
+
edges = store.all_edges()
|
|
101
|
+
|
|
102
|
+
if self._cluster_by:
|
|
103
|
+
lines.extend(self._render_clustered(nodes))
|
|
104
|
+
else:
|
|
105
|
+
for node in nodes:
|
|
106
|
+
lines.append(self._node_def(node))
|
|
107
|
+
lines.append("")
|
|
108
|
+
|
|
109
|
+
for edge in edges:
|
|
110
|
+
lines.append(self._edge_def(edge))
|
|
111
|
+
|
|
112
|
+
lines.append("}")
|
|
113
|
+
return "\n".join(lines) + "\n"
|
|
114
|
+
|
|
115
|
+
# -- internal ---------------------------------------------------
|
|
116
|
+
|
|
117
|
+
def _node_def(self, node: GraphNode) -> str:
|
|
118
|
+
sid = _sanitize_id(node.id)
|
|
119
|
+
style = _NODE_STYLES.get(node.kind, {"shape": "box", "fillcolor": "#FFFFFF", "fontcolor": "black"})
|
|
120
|
+
label = _quote(node.label)
|
|
121
|
+
attrs = (
|
|
122
|
+
f'label="{label}", '
|
|
123
|
+
f'shape={style["shape"]}, '
|
|
124
|
+
f'fillcolor="{style["fillcolor"]}", '
|
|
125
|
+
f'fontcolor="{style["fontcolor"]}"'
|
|
126
|
+
)
|
|
127
|
+
return f" {sid} [{attrs}];"
|
|
128
|
+
|
|
129
|
+
def _edge_def(self, edge: GraphEdge) -> str:
|
|
130
|
+
src = _sanitize_id(edge.source)
|
|
131
|
+
tgt = _sanitize_id(edge.target)
|
|
132
|
+
style = _EDGE_STYLES.get(
|
|
133
|
+
edge.kind,
|
|
134
|
+
{"style": "solid", "arrowhead": "normal"},
|
|
135
|
+
)
|
|
136
|
+
label = _quote(edge.label or edge.kind.value)
|
|
137
|
+
attrs = (
|
|
138
|
+
f'label="{label}", '
|
|
139
|
+
f'style={style["style"]}, '
|
|
140
|
+
f'arrowhead={style["arrowhead"]}'
|
|
141
|
+
)
|
|
142
|
+
return f" {src} -> {tgt} [{attrs}];"
|
|
143
|
+
|
|
144
|
+
def _cluster_key(self, node: GraphNode) -> str:
|
|
145
|
+
if self._cluster_by == "module":
|
|
146
|
+
return node.module or "unknown"
|
|
147
|
+
if self._cluster_by == "domain":
|
|
148
|
+
return node.properties.get("domain", node.module or "unknown") # type: ignore[return-value]
|
|
149
|
+
if self._cluster_by == "node-type":
|
|
150
|
+
return node.kind.value
|
|
151
|
+
return "default"
|
|
152
|
+
|
|
153
|
+
def _render_clustered(self, nodes: list[GraphNode]) -> list[str]:
|
|
154
|
+
clusters: dict[str, list[GraphNode]] = {}
|
|
155
|
+
for node in nodes:
|
|
156
|
+
key = self._cluster_key(node)
|
|
157
|
+
clusters.setdefault(key, []).append(node)
|
|
158
|
+
|
|
159
|
+
lines: list[str] = []
|
|
160
|
+
for idx, (cluster_name, cluster_nodes) in enumerate(sorted(clusters.items())):
|
|
161
|
+
sub_id = _sanitize_id(f"cluster_{cluster_name}")
|
|
162
|
+
lines.append(f" subgraph {sub_id} {{")
|
|
163
|
+
lines.append(f' label="{_quote(cluster_name)}";')
|
|
164
|
+
lines.append(' style=filled;')
|
|
165
|
+
lines.append(' color="#E8E8E8";')
|
|
166
|
+
for node in cluster_nodes:
|
|
167
|
+
lines.append(f" {self._node_def(node)}")
|
|
168
|
+
lines.append(" }")
|
|
169
|
+
lines.append("")
|
|
170
|
+
|
|
171
|
+
return lines
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Mermaid diagram renderer for the OSSCodeIQ graph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from osscodeiq.graph.store import GraphStore
|
|
9
|
+
from osscodeiq.models.graph import EdgeKind, GraphEdge, GraphNode, NodeKind
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _sanitize_id(raw: str) -> str:
|
|
13
|
+
"""Replace characters that are invalid in Mermaid node IDs."""
|
|
14
|
+
return re.sub(r"[^a-zA-Z0-9_]", "_", raw)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _escape_label(text: str) -> str:
|
|
18
|
+
"""Escape Mermaid special characters in labels."""
|
|
19
|
+
for ch in ('"', '|', '[', ']', '{', '}', '(', ')', '<', '>', '#'):
|
|
20
|
+
text = text.replace(ch, f"&#{ord(ch)};")
|
|
21
|
+
return text
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# -- Node shape templates --------------------------------------------------
|
|
25
|
+
# Mermaid syntax: id[label], id([label]), id{{label}}, id[(label)], etc.
|
|
26
|
+
|
|
27
|
+
_NODE_SHAPES: dict[NodeKind, tuple[str, str]] = {
|
|
28
|
+
NodeKind.MODULE: ("[", "]"), # rectangle
|
|
29
|
+
NodeKind.PACKAGE: ("[", "]"),
|
|
30
|
+
NodeKind.CLASS: ("[", "]"),
|
|
31
|
+
NodeKind.METHOD: ("([", "])"), # stadium / pill
|
|
32
|
+
NodeKind.ENDPOINT: ("{{", "}}"), # hexagon
|
|
33
|
+
NodeKind.ENTITY: ("[(", ")]"), # cylinder
|
|
34
|
+
NodeKind.REPOSITORY: ("[(", ")]"),
|
|
35
|
+
NodeKind.QUERY: ("([", "])"),
|
|
36
|
+
NodeKind.MIGRATION: ("([", "])"),
|
|
37
|
+
NodeKind.TOPIC: ("[/", "/]"), # parallelogram
|
|
38
|
+
NodeKind.QUEUE: ("[/", "/]"),
|
|
39
|
+
NodeKind.EVENT: ("[/", "/]"),
|
|
40
|
+
NodeKind.RMI_INTERFACE: ("[[", "]]"), # subroutine
|
|
41
|
+
NodeKind.CONFIG_FILE: ("([", "])"),
|
|
42
|
+
NodeKind.CONFIG_KEY: ("([", "])"),
|
|
43
|
+
NodeKind.WEBSOCKET_ENDPOINT: ("{{", "}}"),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# -- Edge arrow styles ------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
_EDGE_STYLES: dict[EdgeKind, str] = {
|
|
49
|
+
# Solid arrow -->
|
|
50
|
+
EdgeKind.CALLS: "-->",
|
|
51
|
+
EdgeKind.DEPENDS_ON: "-->",
|
|
52
|
+
EdgeKind.IMPORTS: "-->",
|
|
53
|
+
EdgeKind.INJECTS: "-->",
|
|
54
|
+
EdgeKind.QUERIES: "-->",
|
|
55
|
+
EdgeKind.MAPS_TO: "-->",
|
|
56
|
+
EdgeKind.READS_CONFIG: "-->",
|
|
57
|
+
EdgeKind.MIGRATES: "-->",
|
|
58
|
+
EdgeKind.CONTAINS: "-->",
|
|
59
|
+
EdgeKind.EXPOSES: "-->",
|
|
60
|
+
# Dotted arrow -.->
|
|
61
|
+
EdgeKind.PRODUCES: "-.->",
|
|
62
|
+
EdgeKind.CONSUMES: "-.->",
|
|
63
|
+
EdgeKind.PUBLISHES: "-.->",
|
|
64
|
+
EdgeKind.LISTENS: "-.->",
|
|
65
|
+
EdgeKind.INVOKES_RMI: "-.->",
|
|
66
|
+
EdgeKind.EXPORTS_RMI: "-.->",
|
|
67
|
+
# Open arrowhead --o
|
|
68
|
+
EdgeKind.EXTENDS: "--o",
|
|
69
|
+
EdgeKind.IMPLEMENTS: "--o",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
ClusterBy = Literal["module", "domain", "node-type", None]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class MermaidRenderer:
|
|
77
|
+
"""Render a :class:`GraphStore` as a Mermaid flowchart."""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
direction: str = "LR",
|
|
82
|
+
cluster_by: ClusterBy = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
self._direction = direction
|
|
85
|
+
self._cluster_by = cluster_by
|
|
86
|
+
|
|
87
|
+
# -- public API -------------------------------------------------
|
|
88
|
+
|
|
89
|
+
def render(self, store: GraphStore, cluster_by: ClusterBy | None = None) -> str:
|
|
90
|
+
"""Return a Mermaid flowchart string."""
|
|
91
|
+
effective_cluster = cluster_by or self._cluster_by
|
|
92
|
+
lines: list[str] = [f"graph {self._direction}"]
|
|
93
|
+
|
|
94
|
+
nodes = store.all_nodes()
|
|
95
|
+
edges = store.all_edges()
|
|
96
|
+
|
|
97
|
+
if effective_cluster:
|
|
98
|
+
old = self._cluster_by
|
|
99
|
+
self._cluster_by = effective_cluster
|
|
100
|
+
lines.extend(self._render_clustered(nodes, edges))
|
|
101
|
+
self._cluster_by = old
|
|
102
|
+
else:
|
|
103
|
+
lines.extend(self._render_flat(nodes, edges))
|
|
104
|
+
|
|
105
|
+
return "\n".join(lines) + "\n"
|
|
106
|
+
|
|
107
|
+
# -- internal ---------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def _node_def(self, node: GraphNode) -> str:
|
|
110
|
+
sid = _sanitize_id(node.id)
|
|
111
|
+
left, right = _NODE_SHAPES.get(node.kind, ("[", "]"))
|
|
112
|
+
label = _escape_label(node.label)
|
|
113
|
+
return f" {sid}{left}\"{label}\"{right}"
|
|
114
|
+
|
|
115
|
+
def _edge_def(self, edge: GraphEdge) -> str:
|
|
116
|
+
src = _sanitize_id(edge.source)
|
|
117
|
+
tgt = _sanitize_id(edge.target)
|
|
118
|
+
arrow = _EDGE_STYLES.get(edge.kind, "-->")
|
|
119
|
+
label = _escape_label(edge.label or edge.kind.value)
|
|
120
|
+
return f" {src} {arrow}|{label}| {tgt}"
|
|
121
|
+
|
|
122
|
+
def _cluster_key(self, node: GraphNode) -> str:
|
|
123
|
+
if self._cluster_by == "module":
|
|
124
|
+
return node.module or "unknown"
|
|
125
|
+
if self._cluster_by == "domain":
|
|
126
|
+
return node.properties.get("domain", node.module or "unknown") # type: ignore[return-value]
|
|
127
|
+
if self._cluster_by == "node-type":
|
|
128
|
+
return node.kind.value
|
|
129
|
+
return "default"
|
|
130
|
+
|
|
131
|
+
def _render_flat(
|
|
132
|
+
self, nodes: list[GraphNode], edges: list[GraphEdge]
|
|
133
|
+
) -> list[str]:
|
|
134
|
+
lines: list[str] = []
|
|
135
|
+
for node in nodes:
|
|
136
|
+
lines.append(self._node_def(node))
|
|
137
|
+
for edge in edges:
|
|
138
|
+
lines.append(self._edge_def(edge))
|
|
139
|
+
return lines
|
|
140
|
+
|
|
141
|
+
def _render_clustered(
|
|
142
|
+
self, nodes: list[GraphNode], edges: list[GraphEdge]
|
|
143
|
+
) -> list[str]:
|
|
144
|
+
clusters: dict[str, list[GraphNode]] = {}
|
|
145
|
+
for node in nodes:
|
|
146
|
+
key = self._cluster_key(node)
|
|
147
|
+
clusters.setdefault(key, []).append(node)
|
|
148
|
+
|
|
149
|
+
lines: list[str] = []
|
|
150
|
+
for idx, (cluster_name, cluster_nodes) in enumerate(sorted(clusters.items())):
|
|
151
|
+
sub_id = _sanitize_id(f"cluster_{cluster_name}")
|
|
152
|
+
lines.append(f" subgraph {sub_id}[\"{cluster_name}\"]")
|
|
153
|
+
for node in cluster_nodes:
|
|
154
|
+
lines.append(f" {self._node_def(node)}")
|
|
155
|
+
lines.append(" end")
|
|
156
|
+
|
|
157
|
+
for edge in edges:
|
|
158
|
+
lines.append(self._edge_def(edge))
|
|
159
|
+
|
|
160
|
+
return lines
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Graph size safety guard for the OSSCodeIQ CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from osscodeiq.graph.store import GraphStore
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def check_graph_size(
|
|
12
|
+
store: GraphStore,
|
|
13
|
+
max_nodes: int,
|
|
14
|
+
console: Console,
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Abort with a helpful message if *store* exceeds *max_nodes*.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
store:
|
|
21
|
+
The graph store to check.
|
|
22
|
+
max_nodes:
|
|
23
|
+
Maximum number of nodes allowed before the safety guard fires.
|
|
24
|
+
console:
|
|
25
|
+
Rich console used to print the error and suggestions.
|
|
26
|
+
|
|
27
|
+
Raises
|
|
28
|
+
------
|
|
29
|
+
typer.Exit
|
|
30
|
+
If the node count exceeds *max_nodes*.
|
|
31
|
+
"""
|
|
32
|
+
count = store.node_count
|
|
33
|
+
if count <= max_nodes:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
console.print(
|
|
37
|
+
f"\n[bold red]Error:[/bold red] Graph contains "
|
|
38
|
+
f"[bold]{count:,}[/bold] nodes, which exceeds the "
|
|
39
|
+
f"safety limit of [bold]{max_nodes:,}[/bold].\n"
|
|
40
|
+
)
|
|
41
|
+
console.print("[bold yellow]Suggestions to reduce the graph size:[/bold yellow]\n")
|
|
42
|
+
console.print(
|
|
43
|
+
" 1. Use [cyan]--view architect[/cyan] to collapse detail "
|
|
44
|
+
"nodes into module-level nodes."
|
|
45
|
+
)
|
|
46
|
+
console.print(
|
|
47
|
+
" 2. Use [cyan]--focus \"node_id\" --hops 1[/cyan] to restrict "
|
|
48
|
+
"output to a small neighbourhood."
|
|
49
|
+
)
|
|
50
|
+
console.print(
|
|
51
|
+
" 3. Use [cyan]--module <name>[/cyan] to filter by module."
|
|
52
|
+
)
|
|
53
|
+
console.print(
|
|
54
|
+
" 4. Use [cyan]--max-nodes N[/cyan] to override this limit "
|
|
55
|
+
"(e.g. [cyan]--max-nodes 2000[/cyan]).\n"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""JSON and YAML serializers for the OSSCodeIQ graph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from osscodeiq.models.graph import CodeGraph
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class JsonSerializer:
|
|
13
|
+
"""Serialize a :class:`CodeGraph` to JSON."""
|
|
14
|
+
|
|
15
|
+
def serialize(self, graph: CodeGraph, pretty: bool = True) -> str:
|
|
16
|
+
"""Return the graph as a JSON string.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
graph:
|
|
21
|
+
The code graph to serialize.
|
|
22
|
+
pretty:
|
|
23
|
+
When ``True`` (default), emit indented, human-readable JSON.
|
|
24
|
+
"""
|
|
25
|
+
data = graph.model_dump(mode="json")
|
|
26
|
+
if pretty:
|
|
27
|
+
return json.dumps(data, indent=2, sort_keys=False)
|
|
28
|
+
return json.dumps(data, sort_keys=False)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class YamlSerializer:
|
|
32
|
+
"""Serialize a :class:`CodeGraph` to YAML."""
|
|
33
|
+
|
|
34
|
+
def serialize(self, graph: CodeGraph) -> str:
|
|
35
|
+
"""Return the graph as a YAML string."""
|
|
36
|
+
data = graph.model_dump(mode="json")
|
|
37
|
+
return yaml.dump(
|
|
38
|
+
data,
|
|
39
|
+
default_flow_style=False,
|
|
40
|
+
sort_keys=False,
|
|
41
|
+
allow_unicode=True,
|
|
42
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Abstract language support protocol for tree-sitter based parsing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
import tree_sitter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@runtime_checkable
|
|
11
|
+
class LanguageSupport(Protocol):
|
|
12
|
+
"""Protocol that language plugins must satisfy."""
|
|
13
|
+
|
|
14
|
+
name: str
|
|
15
|
+
extensions: tuple[str, ...]
|
|
16
|
+
|
|
17
|
+
def get_language(self) -> tree_sitter.Language:
|
|
18
|
+
"""Return the tree-sitter Language object for this language."""
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
def get_queries(self) -> dict[str, str]:
|
|
22
|
+
"""Return a mapping of query-name to tree-sitter query source."""
|
|
23
|
+
...
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Java language support for tree-sitter parsing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tree_sitter
|
|
6
|
+
import tree_sitter_java
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class JavaLanguageSupport:
|
|
10
|
+
"""Java language support with Spring-focused queries."""
|
|
11
|
+
|
|
12
|
+
name: str = "java"
|
|
13
|
+
extensions: tuple[str, ...] = (".java",)
|
|
14
|
+
|
|
15
|
+
def get_language(self) -> tree_sitter.Language:
|
|
16
|
+
return tree_sitter.Language(tree_sitter_java.language())
|
|
17
|
+
|
|
18
|
+
def get_queries(self) -> dict[str, str]:
|
|
19
|
+
return _JAVA_QUERIES.copy()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------
|
|
23
|
+
# Tree-sitter queries targeting Java + Spring annotation patterns
|
|
24
|
+
# ---------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
_JAVA_QUERIES: dict[str, str] = {
|
|
27
|
+
"annotations": """
|
|
28
|
+
(marker_annotation
|
|
29
|
+
name: (identifier) @annotation.name)
|
|
30
|
+
(annotation
|
|
31
|
+
name: (identifier) @annotation.name
|
|
32
|
+
arguments: (annotation_argument_list) @annotation.args)
|
|
33
|
+
""",
|
|
34
|
+
"class_declarations": """
|
|
35
|
+
(class_declaration
|
|
36
|
+
name: (identifier) @class.name
|
|
37
|
+
superclass: (superclass)? @class.superclass
|
|
38
|
+
interfaces: (super_interfaces)? @class.interfaces
|
|
39
|
+
body: (class_body) @class.body)
|
|
40
|
+
""",
|
|
41
|
+
"method_declarations": """
|
|
42
|
+
(method_declaration
|
|
43
|
+
(modifiers)? @method.modifiers
|
|
44
|
+
type: (_) @method.return_type
|
|
45
|
+
name: (identifier) @method.name
|
|
46
|
+
parameters: (formal_parameters) @method.params
|
|
47
|
+
body: (block)? @method.body)
|
|
48
|
+
""",
|
|
49
|
+
"interface_declarations": """
|
|
50
|
+
(interface_declaration
|
|
51
|
+
name: (identifier) @interface.name
|
|
52
|
+
body: (interface_body) @interface.body)
|
|
53
|
+
""",
|
|
54
|
+
"field_declarations": """
|
|
55
|
+
(field_declaration
|
|
56
|
+
(modifiers)? @field.modifiers
|
|
57
|
+
type: (_) @field.type
|
|
58
|
+
declarator: (variable_declarator
|
|
59
|
+
name: (identifier) @field.name))
|
|
60
|
+
""",
|
|
61
|
+
"import_declarations": """
|
|
62
|
+
(import_declaration
|
|
63
|
+
(scoped_identifier) @import.path)
|
|
64
|
+
""",
|
|
65
|
+
"string_literals": """
|
|
66
|
+
(string_literal) @string.value
|
|
67
|
+
""",
|
|
68
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Python language support for tree-sitter parsing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tree_sitter
|
|
6
|
+
import tree_sitter_python
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PythonLanguageSupport:
|
|
10
|
+
"""Tree-sitter language support for Python."""
|
|
11
|
+
|
|
12
|
+
name: str = "python"
|
|
13
|
+
extensions: tuple[str, ...] = (".py",)
|
|
14
|
+
|
|
15
|
+
def get_language(self) -> tree_sitter.Language:
|
|
16
|
+
return tree_sitter.Language(tree_sitter_python.language())
|
|
17
|
+
|
|
18
|
+
def get_queries(self) -> dict[str, str]:
|
|
19
|
+
return PYTHON_QUERIES
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
PYTHON_QUERIES: dict[str, str] = {
|
|
23
|
+
"function_definitions": """
|
|
24
|
+
(function_definition
|
|
25
|
+
name: (identifier) @func.name
|
|
26
|
+
parameters: (parameters) @func.params
|
|
27
|
+
body: (block) @func.body)
|
|
28
|
+
""",
|
|
29
|
+
"class_definitions": """
|
|
30
|
+
(class_definition
|
|
31
|
+
name: (identifier) @class.name
|
|
32
|
+
body: (block) @class.body)
|
|
33
|
+
""",
|
|
34
|
+
"decorators": """
|
|
35
|
+
(decorator
|
|
36
|
+
(call
|
|
37
|
+
function: (_) @decorator.name
|
|
38
|
+
arguments: (argument_list)? @decorator.args)?)
|
|
39
|
+
""",
|
|
40
|
+
"import_statements": """
|
|
41
|
+
(import_statement
|
|
42
|
+
name: (dotted_name) @import.name)
|
|
43
|
+
""",
|
|
44
|
+
"import_from": """
|
|
45
|
+
(import_from_statement
|
|
46
|
+
module_name: (dotted_name)? @import.module
|
|
47
|
+
name: (_)? @import.name)
|
|
48
|
+
""",
|
|
49
|
+
"string_literals": """
|
|
50
|
+
(string) @string
|
|
51
|
+
""",
|
|
52
|
+
"assignments": """
|
|
53
|
+
(assignment
|
|
54
|
+
left: (_) @assign.target
|
|
55
|
+
right: (_) @assign.value)
|
|
56
|
+
""",
|
|
57
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""TypeScript/JavaScript language support for tree-sitter parsing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tree_sitter
|
|
6
|
+
import tree_sitter_typescript
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TypeScriptLanguageSupport:
|
|
10
|
+
"""Tree-sitter language support for TypeScript."""
|
|
11
|
+
|
|
12
|
+
name: str = "typescript"
|
|
13
|
+
extensions: tuple[str, ...] = (".ts", ".tsx")
|
|
14
|
+
|
|
15
|
+
def get_language(self) -> tree_sitter.Language:
|
|
16
|
+
return tree_sitter.Language(tree_sitter_typescript.language_typescript())
|
|
17
|
+
|
|
18
|
+
def get_queries(self) -> dict[str, str]:
|
|
19
|
+
return TYPESCRIPT_QUERIES
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class JavaScriptLanguageSupport:
|
|
23
|
+
"""Tree-sitter language support for JavaScript."""
|
|
24
|
+
|
|
25
|
+
name: str = "javascript"
|
|
26
|
+
extensions: tuple[str, ...] = (".js", ".jsx")
|
|
27
|
+
|
|
28
|
+
def get_language(self) -> tree_sitter.Language:
|
|
29
|
+
import tree_sitter_javascript
|
|
30
|
+
return tree_sitter.Language(tree_sitter_javascript.language())
|
|
31
|
+
|
|
32
|
+
def get_queries(self) -> dict[str, str]:
|
|
33
|
+
return JAVASCRIPT_QUERIES
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
TYPESCRIPT_QUERIES: dict[str, str] = {
|
|
37
|
+
"class_declarations": """
|
|
38
|
+
(class_declaration
|
|
39
|
+
name: (type_identifier) @class.name
|
|
40
|
+
body: (class_body) @class.body)
|
|
41
|
+
""",
|
|
42
|
+
"method_definitions": """
|
|
43
|
+
(method_definition
|
|
44
|
+
name: (property_identifier) @method.name
|
|
45
|
+
parameters: (formal_parameters) @method.params)
|
|
46
|
+
""",
|
|
47
|
+
"function_declarations": """
|
|
48
|
+
(function_declaration
|
|
49
|
+
name: (identifier) @func.name
|
|
50
|
+
parameters: (formal_parameters) @func.params)
|
|
51
|
+
""",
|
|
52
|
+
"decorators": """
|
|
53
|
+
(decorator
|
|
54
|
+
(call_expression
|
|
55
|
+
function: (_) @decorator.name
|
|
56
|
+
arguments: (arguments)? @decorator.args))
|
|
57
|
+
""",
|
|
58
|
+
"import_statements": """
|
|
59
|
+
(import_statement
|
|
60
|
+
source: (string) @import.source)
|
|
61
|
+
""",
|
|
62
|
+
"call_expressions": """
|
|
63
|
+
(call_expression
|
|
64
|
+
function: (_) @call.func
|
|
65
|
+
arguments: (arguments) @call.args)
|
|
66
|
+
""",
|
|
67
|
+
"string_literals": """
|
|
68
|
+
(string) @string
|
|
69
|
+
""",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
JAVASCRIPT_QUERIES: dict[str, str] = {
|
|
73
|
+
"function_declarations": """
|
|
74
|
+
(function_declaration
|
|
75
|
+
name: (identifier) @func.name
|
|
76
|
+
parameters: (formal_parameters) @func.params)
|
|
77
|
+
""",
|
|
78
|
+
"class_declarations": """
|
|
79
|
+
(class_declaration
|
|
80
|
+
name: (identifier) @class.name
|
|
81
|
+
body: (class_body) @class.body)
|
|
82
|
+
""",
|
|
83
|
+
"call_expressions": """
|
|
84
|
+
(call_expression
|
|
85
|
+
function: (_) @call.func
|
|
86
|
+
arguments: (arguments) @call.args)
|
|
87
|
+
""",
|
|
88
|
+
"import_statements": """
|
|
89
|
+
(import_statement
|
|
90
|
+
source: (string) @import.source)
|
|
91
|
+
""",
|
|
92
|
+
"string_literals": """
|
|
93
|
+
(string) @string
|
|
94
|
+
""",
|
|
95
|
+
}
|