tiny.smg.exports
Defined in tiny.smg.
API (10)
Actions
Public operations.
isArchitectureCoupling: Returns whether an edge belongs in architecture coupling metrics.isCouplingisNamespaceHandleImportnodeJsontoDottoDsmtoJsontoMermaidtoText
Values and defaults
Public values and defaults.
Source
Source: tools/smg/src/export.zig
zig
const std = @import("std");const graph_mod = @import("graph.zig");const api = @import("api.zig");const view = @import("view.zig");const model = @import("model.zig");const pretty = @import("pretty");const json = pretty.json;pub fn toJson(allocator: std.mem.Allocator, graph: graph_mod.Graph, indent: bool) ![]const u8 { var out: std.Io.Writer.Allocating = .init(allocator); errdefer out.deinit(); const nodes = try view.allNodes(graph, allocator, null); const edges = try view.allEdges(graph, allocator); var writer = json.Writer.init(&out.writer, if (indent) .indent_2 else .minified); try writer.beginObject(); try writer.objectField("nodes"); try writer.beginArray(); for (nodes) |node| try writeNodeDataObject(allocator, &writer, node); try writer.endArray(); try writer.objectField("edges"); try writer.beginArray(); for (edges) |edge| try writeEdgeDataObject(allocator, &writer, edge); try writer.endArray(); try writer.endObject(); try out.writer.writeByte('\n'); return try out.toOwnedSlice();}pub fn nodeJson(allocator: std.mem.Allocator, node: model.Node, incoming: []const model.Edge, outgoing: []const model.Edge) ![]const u8 { var out: std.Io.Writer.Allocating = .init(allocator); errdefer out.deinit(); var writer = json.Writer.init(&out.writer, .minified); try writer.beginObject(); try writeNodeDataFields(allocator, node, &writer); try writer.objectField("incoming"); try writer.beginArray(); for (incoming) |edge| { try writer.beginObject(); try writer.objectField("source"); try writer.write(edge.source); try writer.objectField("rel"); try writer.write(edge.rel); try writer.endObject(); } try writer.endArray(); try writer.objectField("outgoing"); try writer.beginArray(); for (outgoing) |edge| { try writer.beginObject(); try writer.objectField("target"); try writer.write(edge.target); try writer.objectField("rel"); try writer.write(edge.rel); try writer.endObject(); } try writer.endArray(); try writer.endObject(); try out.writer.writeByte('\n'); return try out.toOwnedSlice();}fn writeNodeDataObject(allocator: std.mem.Allocator, writer: *json.Writer, node: model.Node) !void { try writer.beginObject(); try writeNodeDataFields(allocator, node, writer); try writer.endObject();}fn writeNodeDataFields(allocator: std.mem.Allocator, node: model.Node, writer: *json.Writer) !void { try writer.objectField("name"); try writer.write(node.name); try writer.objectField("type"); try writer.write(node.type); if (node.file) |file| { try writer.objectField("file"); try writer.write(file); } if (node.line) |line| { try writer.objectField("line"); try writer.write(line); } if (node.end_line) |line| { try writer.objectField("end_line"); try writer.write(line); } if (node.docstring) |doc| { try writer.objectField("docstring"); try writer.write(doc); } if (node.metadata.len != 0) try writeMetadata(allocator, writer, node.metadata);}fn writeEdgeDataObject(allocator: std.mem.Allocator, writer: *json.Writer, edge: model.Edge) !void { try writer.beginObject(); try writer.objectField("source"); try writer.write(edge.source); try writer.objectField("rel"); try writer.write(edge.rel); try writer.objectField("target"); try writer.write(edge.target); if (edge.metadata.len != 0) try writeMetadata(allocator, writer, edge.metadata); try writer.endObject();}fn writeMetadata(allocator: std.mem.Allocator, writer: *json.Writer, metadata: []const model.Pair) !void { try writer.objectField("metadata"); try writer.beginObject(); for (metadata) |pair| { try writer.objectField(pair.key); if (pair.json) { var parsed = std.json.parseFromSlice(std.json.Value, allocator, pair.value, .{}) catch { try writer.raw(pair.value); continue; }; defer parsed.deinit(); try writer.write(parsed.value); } else { try writer.write(pair.value); } } try writer.endObject();}pub fn toText(allocator: std.mem.Allocator, graph: graph_mod.Graph) ![]const u8 { var out: std.Io.Writer.Allocating = .init(allocator); errdefer out.deinit(); const nodes = try view.allNodes(graph, allocator, null); if (nodes.len == 0) return try allocator.dupe(u8, "Empty graph."); const edges = try view.allEdges(graph, allocator); try out.writer.print("Nodes ({d}):\n", .{nodes.len}); for (nodes) |node| { try out.writer.print(" [{s}] {s}", .{ node.type, node.name }); if (node.file) |file| { try out.writer.print("\n @ {s}", .{file}); if (node.line) |line| try out.writer.print(":{d}", .{line}); } if (node.docstring) |doc| try out.writer.print("\n # {s}", .{doc}); try out.writer.writeByte('\n'); } if (edges.len != 0) { try out.writer.print("\nEdges ({d}):\n", .{edges.len}); for (edges) |edge| try out.writer.print(" {s} --{s}--> {s}\n", .{ edge.source, edge.rel, edge.target }); } return try out.toOwnedSlice();}pub fn toMermaid(allocator: std.mem.Allocator, graph: graph_mod.Graph) ![]const u8 { var out: std.Io.Writer.Allocating = .init(allocator); errdefer out.deinit(); const nodes = try view.allNodes(graph, allocator, null); const edges = try view.allEdges(graph, allocator); try out.writer.writeAll("graph TD\n"); for (nodes) |node| { try out.writer.print(" {s}[\"{s} ({s})\"]\n", .{ try ident(allocator, node.name), node.name, node.type }); } for (edges) |edge| { try out.writer.print(" {s} -->|{s}| {s}\n", .{ try ident(allocator, edge.source), edge.rel, try ident(allocator, edge.target) }); } return try out.toOwnedSlice();}pub fn toDot(allocator: std.mem.Allocator, graph: graph_mod.Graph) ![]const u8 { var out: std.Io.Writer.Allocating = .init(allocator); errdefer out.deinit(); const nodes = try view.allNodes(graph, allocator, null); const edges = try view.allEdges(graph, allocator); try out.writer.writeAll("digraph smg {\n rankdir=LR;\n"); for (nodes) |node| { try out.writer.print(" {s} [label=\"{s}\\n({s})\"];\n", .{ try ident(allocator, node.name), node.name, node.type }); } for (edges) |edge| { try out.writer.print(" {s} -> {s} [label=\"{s}\"];\n", .{ try ident(allocator, edge.source), try ident(allocator, edge.target), edge.rel }); } try out.writer.writeAll("}\n"); return try out.toOwnedSlice();}pub fn toDsm(allocator: std.mem.Allocator, graph: graph_mod.Graph, level: []const u8) ![]const u8 { const nodes = try view.allNodes(graph, allocator, null); const edges = try view.allEdges(graph, allocator); var names: std.ArrayList([]const u8) = .empty; for (nodes) |node| { if (std.mem.eql(u8, level, "all") or std.mem.eql(u8, node.type, model.NodeType.module) or std.mem.eql(u8, node.type, model.NodeType.package) or (std.mem.eql(u8, level, "class") and std.mem.eql(u8, node.type, model.NodeType.class))) { try names.append(allocator, node.name); } } if (names.items.len == 0) return try allocator.dupe(u8, ""); var map = std.StringHashMap([]const u8).init(allocator); for (nodes) |node| { if (containsName(names.items, node.name)) { try map.put(node.name, node.name); continue; } if (std.mem.eql(u8, level, "all")) continue; if (try containingOwner(allocator, graph, node.name, names.items)) |owner_name| try map.put(node.name, owner_name); } var out: std.Io.Writer.Allocating = .init(allocator); errdefer out.deinit(); try out.writer.writeAll(""); for (names.items) |name| { try out.writer.writeByte(','); try out.writer.writeAll(name); } try out.writer.writeByte('\n'); for (names.items) |row| { try out.writer.writeAll(row); for (names.items) |col| { var count: usize = 0; for (edges) |edge| { if (!isCoupling(edge.rel)) continue; const source = map.get(edge.source) orelse continue; const target = map.get(edge.target) orelse continue; if (!std.mem.eql(u8, source, target) and std.mem.eql(u8, source, row) and std.mem.eql(u8, target, col)) count += 1; } try out.writer.print(",{d}", .{count}); } try out.writer.writeByte('\n'); } return try out.toOwnedSlice();}pub fn isCoupling(rel: []const u8) bool { return std.mem.eql(u8, rel, model.RelType.calls) or std.mem.eql(u8, rel, model.RelType.imports) or std.mem.eql(u8, rel, model.RelType.inherits) or std.mem.eql(u8, rel, model.RelType.implements) or std.mem.eql(u8, rel, model.RelType.depends_on);}pub const coupling_relations: []const []const u8 = relations: { const names = @typeInfo(model.RelType).@"struct".decl_names; var selected: [names.len][]const u8 = undefined; var count: usize = 0; for (names) |name| { const relation: []const u8 = @field(model.RelType, name); if (!isCoupling(relation)) continue; selected[count] = relation; count += 1; } const bound = selected[0..count].*; break :relations &bound;};pub fn isNamespaceHandleImport(rel: []const u8, source: []const u8, target: []const u8) bool { if (!std.mem.eql(u8, rel, model.RelType.imports)) return false; if (std.mem.eql(u8, source, target)) return false; if (!std.mem.endsWith(u8, target, ".root")) return false; const namespace = target[0 .. target.len - ".root".len]; if (source.len <= namespace.len + 1) return false; if (!std.mem.startsWith(u8, source, namespace)) return false; return source[namespace.len] == '.';}/// Returns whether an edge belongs in architecture coupling metrics.////// Raw graph queries and exports retain namespace-handle and test edges.pub fn isArchitectureCoupling(graph: graph_mod.Graph, edge: model.Edge) bool { if (!isCoupling(edge.rel)) return false; if (isNamespaceHandleImport(edge.rel, edge.source, edge.target)) return false; if (graph_mod.getNode(&graph, edge.source)) |node| { if (api.isTestDeclaration(node)) return false; } if (graph_mod.getNode(&graph, edge.target)) |node| { if (api.isTestDeclaration(node)) return false; } return true;}test "coupling relation table matches the coupling predicate" { const names = @typeInfo(model.RelType).@"struct".decl_names; var coupling: usize = 0; inline for (names) |name| { const relation: []const u8 = @field(model.RelType, name); var listed = false; for (coupling_relations) |candidate| { if (std.mem.eql(u8, candidate, relation)) listed = true; } try std.testing.expectEqual(isCoupling(relation), listed); if (listed) coupling += 1; } try std.testing.expectEqual(coupling, coupling_relations.len);}test "namespace handle imports cover ancestor handles but not cross-package roots" { try std.testing.expect(isNamespaceHandleImport("imports", "lib.agent.src.facet", "lib.agent.src.root")); try std.testing.expect(isNamespaceHandleImport("imports", "lib.agent.src.facets.lisp", "lib.agent.src.root")); try std.testing.expect(!isNamespaceHandleImport("imports", "lib.lang.src.runtime.bindings", "lib.preserves.src.root")); try std.testing.expect(!isNamespaceHandleImport("imports", "lib.agent.src.root", "lib.agent.src.facet")); try std.testing.expect(!isNamespaceHandleImport("calls", "lib.agent.src.facet.step", "lib.agent.src.root.Event")); try std.testing.expect(!isNamespaceHandleImport("imports", "lib.agent.src.root", "lib.agent.src.root"));}test "architecture coupling excludes namespace handles and test declarations" { var graph = graph_mod.init(std.testing.allocator); defer graph_mod.deinit(&graph); const test_metadata = [_]model.Pair{.{ .key = api.test_key, .value = "true" }}; try graph_mod.addNode(&graph, .{ .name = "pkg.root", .type = model.NodeType.module }); try graph_mod.addNode(&graph, .{ .name = "pkg.worker", .type = model.NodeType.function }); try graph_mod.addNode(&graph, .{ .name = "pkg.test_worker", .type = model.NodeType.function, .metadata = &test_metadata }); const handle: model.Edge = .{ .source = "pkg.worker", .rel = model.RelType.imports, .target = "pkg.root" }; const production: model.Edge = .{ .source = "pkg.root", .rel = model.RelType.calls, .target = "pkg.worker" }; const test_call: model.Edge = .{ .source = "pkg.test_worker", .rel = model.RelType.calls, .target = "pkg.worker" }; try std.testing.expect(isCoupling(handle.rel)); try std.testing.expect(isCoupling(test_call.rel)); try std.testing.expect(!isArchitectureCoupling(graph, handle)); try std.testing.expect(isArchitectureCoupling(graph, production)); try std.testing.expect(!isArchitectureCoupling(graph, test_call));}fn ident(allocator: std.mem.Allocator, value: []const u8) ![]const u8 { const out = try allocator.dupe(u8, value); for (out) |*byte| { if (!std.ascii.isAlphanumeric(byte.*)) byte.* = '_'; } return out;}fn csvCell(writer: *std.Io.Writer, value: []const u8) !void { if (std.mem.indexOfAny(u8, value, ",\"\n") == null) { try writer.writeAll(value); return; } try writer.writeByte('"'); for (value) |byte| { if (byte == '"') try writer.writeByte('"'); try writer.writeByte(byte); } try writer.writeByte('"');}fn cmpString(_: void, a: []const u8, b: []const u8) bool { return std.mem.lessThan(u8, a, b);}fn containsName(names: []const []const u8, name: []const u8) bool { for (names) |item| if (std.mem.eql(u8, item, name)) return true; return false;}fn containingOwner(allocator: std.mem.Allocator, graph: graph_mod.Graph, name: []const u8, names: []const []const u8) !?[]const u8 { var current = name; while (true) { const incoming = try view.incoming(graph, allocator, current, model.RelType.contains); if (incoming.len == 0) return null; current = incoming[0].source; if (containsName(names, current)) return current; }}fn sampleGraph(allocator: std.mem.Allocator) !graph_mod.Graph { var graph = graph_mod.init(allocator); try graph_mod.addNode(&graph, .{ .name = "app", .type = model.NodeType.module }); try graph_mod.addNode(&graph, .{ .name = "app.main", .type = model.NodeType.function, .file = "app.py", .line = 1 }); try graph_mod.addEdge(&graph, .{ .source = "app", .target = "app.main", .rel = model.RelType.contains }); return graph;}test "json export omits storage kind" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const graph = try sampleGraph(allocator); const rendered_json = try toJson(allocator, graph, false); try std.testing.expect(std.mem.indexOf(u8, rendered_json, "\"kind\"") == null); try std.testing.expect(std.mem.indexOf(u8, rendered_json, "\"nodes\"") != null); try std.testing.expect(std.mem.indexOf(u8, rendered_json, "\"edges\"") != null); const pretty_json = try toJson(allocator, graph, true); try std.testing.expectEqualStrings( \\{ \\ "nodes": [ \\ { \\ "name": "app", \\ "type": "module" \\ }, \\ { \\ "name": "app.main", \\ "type": "function", \\ "file": "app.py", \\ "line": 1 \\ } \\ ], \\ "edges": [ \\ { \\ "source": "app", \\ "rel": "contains", \\ "target": "app.main" \\ } \\ ] \\} \\ , pretty_json);}test "indented json export renders nested metadata through pretty json" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const metadata = [_]model.Pair{ .{ .key = "metrics", .value = "{\"fan_in\":1,\"tags\":[\"alpha\",2]}", .json = true }, .{ .key = "source", .value = "manual" }, }; var graph = graph_mod.init(allocator); try graph_mod.addNode(&graph, .{ .name = "app", .type = model.NodeType.module, .metadata = &metadata }); const pretty_json = try toJson(allocator, graph, true); try std.testing.expect(std.mem.indexOf(u8, pretty_json, \\ "metadata": { \\ "metrics": { \\ "fan_in": 1, \\ "tags": [ \\ "alpha", \\ 2 \\ ] \\ }, \\ "source": "manual" \\ } ) != null);}test "text mermaid dot and dsm match python contract markers" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const empty = graph_mod.init(allocator); try std.testing.expectEqualStrings("Empty graph.", try toText(allocator, empty)); var graph = try sampleGraph(allocator); const text_out = try toText(allocator, graph); try std.testing.expect(std.mem.indexOf(u8, text_out, "Nodes (2):") != null); try std.testing.expect(std.mem.indexOf(u8, text_out, "[module] app") != null); const mermaid = try toMermaid(allocator, graph); try std.testing.expect(std.mem.startsWith(u8, mermaid, "graph TD")); try std.testing.expect(std.mem.indexOf(u8, mermaid, "-->|contains|") != null); const dot = try toDot(allocator, graph); try std.testing.expect(std.mem.startsWith(u8, dot, "digraph smg {")); try std.testing.expect(std.mem.indexOf(u8, dot, "label=\"contains\"") != null); try graph_mod.addNode(&graph, .{ .name = "lib", .type = model.NodeType.module }); try graph_mod.addNode(&graph, .{ .name = "lib.f", .type = model.NodeType.function }); try graph_mod.addEdge(&graph, .{ .source = "lib", .target = "lib.f", .rel = model.RelType.contains }); try graph_mod.addEdge(&graph, .{ .source = "app.main", .target = "lib.f", .rel = model.RelType.calls }); const dsm = try toDsm(allocator, graph, "module"); try std.testing.expect(std.mem.startsWith(u8, dsm, ",app,lib")); try std.testing.expect(std.mem.indexOf(u8, dsm, "app,0,1") != null);}Source: tools/smg/src/root.zig:21
zig
pub const exports = @import("export.zig");Audit
| Definitions | 11 |
|---|---|
| Public names | 11 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |