tiny.smg.scan.script
Defined in scan.
API (3)
Actions
Public operations.
Source
Source: tools/smg/src/scan/root.zig:11
zig
pub const script = @import("script.zig");Source: tools/smg/src/scan/script.zig
zig
const std = @import("std");const smg = @import("../root.zig");const graph_mod = smg.graph;const lines_mod = smg.lines;const model = smg.model;const scan_root = smg.scan;const text = smg.text;const tree = smg.tree;const core = scan_root.core;const metrics = scan_root.scan_metrics;const scan_files = scan_root.scan_files;const CallTarget = core.CallTarget;const SourceLines = lines_mod.SourceLines;const Stats = core.Stats;const braceDelta = metrics.braceDelta;const identByte = metrics.identByte;const javascriptFunctionMetadata = metrics.javascriptFunctionMetadata;const scriptSpanMetadata = metrics.scriptSpanMetadata;const textFunctionMetadata = metrics.textFunctionMetadata;const textSpanMetadata = metrics.textSpanMetadata;const ScriptFunctionScope = struct { name: []const u8, start_line: i64, start_depth: i64, class_name: ?[]const u8 = null,};const ScriptArrowSkipKind = enum { brace, paren,};const ScriptArrowSkip = struct { kind: ScriptArrowSkipKind, brace_depth: i64, paren_depth: i64,};const ScriptScanContext = struct { allocator: std.mem.Allocator, scratch: std.mem.Allocator, graph: *graph_mod.Graph, module: []const u8, path: []const u8, lang: []const u8, stats: *Stats,};const ScriptScanState = struct { depth: i64 = 0, paren_depth: i64 = 0, current_class: ?ScriptFunctionScope = null, current_interface: ?ScriptFunctionScope = null, current_function: ?ScriptFunctionScope = null, skipped_arrow: ?ScriptArrowSkip = null,};const ScriptEnhanceContext = struct { allocator: std.mem.Allocator, scratch: std.mem.Allocator, graph: *graph_mod.Graph, module: []const u8, path: []const u8, source: []const u8, stats: *Stats,};const ScriptBody = struct { fragment: []const u8, braced: bool,};const ScriptMemberObject = struct { value: []const u8, start: usize,};const ScriptDecl = struct { name: []const u8, value: []const u8,};pub fn language(lang: []const u8) bool { return std.mem.eql(u8, lang, "javascript") or std.mem.eql(u8, lang, "typescript");}pub fn treeParseCandidate(source: []const u8) bool { return core.compactParseCandidateAny(source, &.{ "import ", "const ", "let ", "var ", "function ", "async function ", "class ", "interface ", "type ", "export " });}pub fn scan(allocator: std.mem.Allocator, scratch: std.mem.Allocator, graph: *graph_mod.Graph, module: []const u8, path: []const u8, source: []const u8, source_lines: SourceLines, lang: []const u8, stats: *Stats, limits: smg.ParserLimits) !void { const context: ScriptScanContext = .{ .allocator = allocator, .scratch = scratch, .graph = graph, .module = module, .path = path, .lang = lang, .stats = stats }; var state: ScriptScanState = .{}; var lines = std.mem.splitScalar(u8, source, '\n'); var line_no: i64 = 0; while (lines.next()) |raw_line| { line_no += 1; try scanLine(context, &state, raw_line, line_no); } try enhanceMetadataFromTree( allocator, scratch, graph, module, path, source, source_lines, lang, stats, limits, );}fn scanLine(context: ScriptScanContext, state: *ScriptScanState, raw_line: []const u8, line_no: i64) !void { const trimmed = text.trim(raw_line); if (trimmed.len == 0) return; const depth_before = state.depth; const paren_depth_before = state.paren_depth; if (state.skipped_arrow != null) return try continueArrowSkip(context, state, trimmed, line_no); if (state.current_function) |function_scope| { state.skipped_arrow = try extractCallsOutsideNestedArrows(context.allocator, function_scope.name, trimmed, depth_before, paren_depth_before, context.stats, function_scope.class_name); } else if (state.current_class != null and depth_before > state.current_class.?.start_depth) { try scanClassLine(context, state, trimmed, line_no, depth_before, paren_depth_before); } else if (depth_before == 0) { try scanTopLevelLine(context, state, trimmed, line_no, depth_before, paren_depth_before); } try finishLine(context, state, trimmed, line_no);}fn continueArrowSkip(context: ScriptScanContext, state: *ScriptScanState, trimmed: []const u8, line_no: i64) !void { state.depth += braceDelta(trimmed); state.paren_depth += parenDelta(trimmed); const skip = state.skipped_arrow.?; const skip_done = switch (skip.kind) { .brace => state.depth <= skip.brace_depth, .paren => state.paren_depth <= skip.paren_depth, }; if (skip_done) state.skipped_arrow = null; try closeScopes(context, state, line_no); clampDepths(state);}fn scanClassLine(context: ScriptScanContext, state: *ScriptScanState, trimmed: []const u8, line_no: i64, depth_before: i64, paren_depth_before: i64) !void { const class_name = state.current_class.?.name; const name = methodName(trimmed) orelse return; const full = try addMethodNode(context.allocator, context.graph, class_name, context.path, name, line_no, context.stats); if (functionBody(trimmed)) |body| { state.skipped_arrow = try extractCallsOutsideNestedArrows(context.allocator, full, body.fragment, depth_before, paren_depth_before, context.stats, class_name); if (body.braced and braceDelta(trimmed) > 0) state.current_function = .{ .name = full, .start_line = line_no, .start_depth = depth_before, .class_name = class_name }; }}fn scanTopLevelLine(context: ScriptScanContext, state: *ScriptScanState, trimmed: []const u8, line_no: i64, depth_before: i64, paren_depth_before: i64) !void { try scanImportLine(context, trimmed); try scanInterfaceLine(context, state, trimmed, line_no, depth_before); try scanClassDeclarationLine(context, state, trimmed, line_no, depth_before); try scanConstantLine(context, trimmed, line_no); try scanFunctionLine(context, state, trimmed, line_no, depth_before, paren_depth_before);}fn scanImportLine(context: ScriptScanContext, trimmed: []const u8) !void { const target = jsImport(trimmed) orelse return; const resolved_target = try importTarget(context.allocator, context.scratch, target, context.module, context.path) orelse return; try core.addImportEdge(context.allocator, context.graph, context.module, resolved_target, &.{}, context.stats);}fn scanInterfaceLine(context: ScriptScanContext, state: *ScriptScanState, trimmed: []const u8, line_no: i64, depth_before: i64) !void { const name = interfaceName(trimmed) orelse return; const full = try addInterfaceNode(context.allocator, context.graph, context.module, context.path, name, line_no, context.stats); if (braceDelta(trimmed) > 0) state.current_interface = .{ .name = full, .start_line = line_no, .start_depth = depth_before };}fn scanClassDeclarationLine(context: ScriptScanContext, state: *ScriptScanState, trimmed: []const u8, line_no: i64, depth_before: i64) !void { const name = className(trimmed) orelse return; const full = try addClassNode(context.allocator, context.graph, context.module, context.path, name, line_no, context.stats); try addHeritageEdges(context.allocator, context.stats, full, trimmed); if (braceDelta(trimmed) > 0) state.current_class = .{ .name = full, .start_line = line_no, .start_depth = depth_before };}fn scanConstantLine(context: ScriptScanContext, trimmed: []const u8, line_no: i64) !void { const name = constantName(trimmed) orelse return; try addConstantNode(context.allocator, context.graph, context.module, context.path, name, line_no, context.stats);}fn addConstantNode(allocator: std.mem.Allocator, graph: *graph_mod.Graph, module: []const u8, path: []const u8, name: []const u8, line_no: i64, stats: *Stats) !void { const full = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ module, name }); const meta = try model.sourcePair(allocator, "scan"); try graph_mod.addNode(graph, .{ .name = full, .type = model.NodeType.constant, .file = path, .line = line_no, .end_line = line_no, .metadata = meta }); try core.addScanEdge(graph, stats, .{ .source = module, .rel = model.RelType.contains, .target = full, .metadata = meta }); stats.nodes_added += 1; try core.countType(stats, allocator, model.NodeType.constant);}fn scanFunctionLine(context: ScriptScanContext, state: *ScriptScanState, trimmed: []const u8, line_no: i64, depth_before: i64, paren_depth_before: i64) !void { const name = functionName(trimmed) orelse return; const full = try addFunctionNode(context.allocator, context.graph, context.module, context.path, name, line_no, context.stats); if (functionBody(trimmed)) |body| { state.skipped_arrow = try extractCallsOutsideNestedArrows(context.allocator, full, body.fragment, depth_before, paren_depth_before, context.stats, null); if (body.braced) state.current_function = .{ .name = full, .start_line = line_no, .start_depth = depth_before }; }}fn finishLine(context: ScriptScanContext, state: *ScriptScanState, trimmed: []const u8, line_no: i64) !void { state.depth += braceDelta(trimmed); state.paren_depth += parenDelta(trimmed); try closeScopes(context, state, line_no); clampDepths(state);}fn closeScopes(context: ScriptScanContext, state: *ScriptScanState, line_no: i64) !void { if (state.current_function) |function_scope| { if (state.depth <= function_scope.start_depth) { try updateFunctionNodeEnd(context.allocator, context.graph, function_scope.name, context.path, function_scope.start_line, line_no); state.current_function = null; } } if (state.current_class) |class_scope| { if (state.depth <= class_scope.start_depth) { try updateNodeEnd(context.allocator, context.graph, class_scope.name, context.path, class_scope.start_line, line_no); state.current_class = null; } } if (state.current_interface) |interface_scope| { if (state.depth <= interface_scope.start_depth) { try updateNodeEnd(context.allocator, context.graph, interface_scope.name, context.path, interface_scope.start_line, line_no); state.current_interface = null; } }}fn clampDepths(state: *ScriptScanState) void { if (state.depth < 0) state.depth = 0; if (state.paren_depth < 0) state.paren_depth = 0;}fn addFunctionNode(allocator: std.mem.Allocator, graph: *graph_mod.Graph, module: []const u8, path: []const u8, name: []const u8, line_no: i64, stats: *Stats) ![]const u8 { const full = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ module, name }); const meta = try model.sourcePair(allocator, "scan"); try graph_mod.addNode(graph, .{ .name = full, .type = model.NodeType.function, .file = path, .line = line_no, .end_line = line_no, .metadata = meta }); try core.addScanEdge(graph, stats, .{ .source = module, .rel = model.RelType.contains, .target = full, .metadata = meta }); stats.nodes_added += 1; try core.countType(stats, allocator, model.NodeType.function); return full;}fn addClassNode(allocator: std.mem.Allocator, graph: *graph_mod.Graph, module: []const u8, path: []const u8, name: []const u8, line_no: i64, stats: *Stats) ![]const u8 { const full = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ module, name }); const meta = try model.sourcePair(allocator, "scan"); try graph_mod.addNode(graph, .{ .name = full, .type = model.NodeType.class, .file = path, .line = line_no, .end_line = line_no, .metadata = meta }); try core.addScanEdge(graph, stats, .{ .source = module, .rel = model.RelType.contains, .target = full, .metadata = meta }); stats.nodes_added += 1; try core.countType(stats, allocator, model.NodeType.class); return full;}fn addInterfaceNode(allocator: std.mem.Allocator, graph: *graph_mod.Graph, module: []const u8, path: []const u8, name: []const u8, line_no: i64, stats: *Stats) ![]const u8 { const full = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ module, name }); const meta = try model.sourcePair(allocator, "scan"); try graph_mod.addNode(graph, .{ .name = full, .type = model.NodeType.interface, .file = path, .line = line_no, .end_line = line_no, .metadata = meta }); try core.addScanEdge(graph, stats, .{ .source = module, .rel = model.RelType.contains, .target = full, .metadata = meta }); stats.nodes_added += 1; try core.countType(stats, allocator, model.NodeType.interface); return full;}fn addMethodNode(allocator: std.mem.Allocator, graph: *graph_mod.Graph, class_name: []const u8, path: []const u8, name: []const u8, line_no: i64, stats: *Stats) ![]const u8 { const full = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ class_name, name }); const meta = try model.sourcePair(allocator, "scan"); try graph_mod.addNode(graph, .{ .name = full, .type = model.NodeType.method, .file = path, .line = line_no, .end_line = line_no, .metadata = meta }); try core.addScanEdge(graph, stats, .{ .source = class_name, .rel = model.RelType.contains, .target = full, .metadata = meta }); stats.nodes_added += 1; try core.countType(stats, allocator, model.NodeType.method); return full;}fn updateFunctionNodeEnd(allocator: std.mem.Allocator, graph: *graph_mod.Graph, name: []const u8, path: []const u8, line: i64, end_line: i64) !void { try updateNodeEnd(allocator, graph, name, path, line, end_line);}fn updateNodeEnd(allocator: std.mem.Allocator, graph: *graph_mod.Graph, name: []const u8, path: []const u8, line: i64, end_line: i64) !void { _ = allocator; const existing = graph_mod.getNode(graph, name) orelse return; try graph_mod.addNode(graph, .{ .name = name, .type = existing.type, .file = path, .line = line, .end_line = end_line, .metadata = existing.metadata });}fn enhanceMetadataFromTree(allocator: std.mem.Allocator, scratch: std.mem.Allocator, graph: *graph_mod.Graph, module: []const u8, path: []const u8, source: []const u8, source_lines: SourceLines, lang: []const u8, stats: *Stats, limits: smg.ParserLimits) !void { if (!treeParseCandidate(source)) return try enhanceMetadataFromText(allocator, graph, path, source_lines, stats); const parsed: ?tree.Parsed = if (std.mem.eql(u8, lang, "javascript")) try tree.parseJavaScript(scratch, source, limits) else if (std.mem.eql(u8, lang, "typescript") and std.mem.endsWith(u8, path, ".tsx")) try tree.parseTsx(scratch, source, limits) else if (std.mem.eql(u8, lang, "typescript")) try tree.parseTypeScript(scratch, source, limits) else return; const parsed_value = parsed orelse return try enhanceMetadataFromText(allocator, graph, path, source_lines, stats); defer tree.deinit(parsed_value); try enhanceBody(allocator, scratch, graph, module, path, source, tree.root(parsed_value), stats);}fn enhanceMetadataFromText(allocator: std.mem.Allocator, graph: *graph_mod.Graph, path: []const u8, source_lines: SourceLines, stats: *Stats) !void { for (graph.nodes.items) |*node| { const file = node.file orelse continue; if (!std.mem.eql(u8, file, path)) continue; const line = node.line orelse continue; const end_line = node.end_line orelse line; if (std.mem.eql(u8, node.type, model.NodeType.function) or std.mem.eql(u8, node.type, model.NodeType.method)) { node.metadata = try model.mergePairs(allocator, node.metadata, try textFunctionMetadata(allocator, stats.metricInputAllocator(allocator), source_lines, line, end_line, .javascript)); } else if (std.mem.eql(u8, node.type, model.NodeType.class)) { node.metadata = try model.mergePairs(allocator, node.metadata, try textSpanMetadata(allocator, source_lines, line, end_line, null)); } }}fn enhanceBody(allocator: std.mem.Allocator, scratch: std.mem.Allocator, graph: *graph_mod.Graph, module: []const u8, path: []const u8, source: []const u8, node: tree.Node, stats: *Stats) !void { const context: ScriptEnhanceContext = .{ .allocator = allocator, .scratch = scratch, .graph = graph, .module = module, .path = path, .source = source, .stats = stats }; const children = tree.childCount(node); for (0..children) |raw_index| { const child_node = tree.child(node, @intCast(raw_index)); try enhanceChild(context, child_node); }}fn enhanceChild(context: ScriptEnhanceContext, node: tree.Node) anyerror!void { const node_kind = tree.kind(node); if (try enhanceImportChild(context, node_kind, node)) return; if (try enhanceNamedDeclarationChild(context, node_kind, node)) return; if (try enhanceVariableChild(context, node_kind, node)) return; if (std.mem.eql(u8, node_kind, "export_statement")) { try enhanceImport(context.allocator, context.scratch, context.graph, context.module, context.path, context.source, node, context.stats); const children = tree.childCount(node); for (0..children) |raw_index| { const child_node = tree.child(node, @intCast(raw_index)); try enhanceChild(context, child_node); } return; } try enhanceDefaultFunctionChild(context, node_kind, node);}fn enhanceImportChild(context: ScriptEnhanceContext, node_kind: []const u8, node: tree.Node) !bool { if (!std.mem.eql(u8, node_kind, "import_statement")) return false; try enhanceImport(context.allocator, context.scratch, context.graph, context.module, context.path, context.source, node, context.stats); return true;}fn enhanceNamedDeclarationChild(context: ScriptEnhanceContext, node_kind: []const u8, node: tree.Node) !bool { if (std.mem.eql(u8, node_kind, "class_declaration")) { try enhanceClassDeclaration(context, node); return true; } if (std.mem.eql(u8, node_kind, "interface_declaration")) { try enhanceNamedInterface(context, node); return true; } if (std.mem.eql(u8, node_kind, "function_declaration")) { try enhanceNamedFunction(context, node); return true; } return false;}fn enhanceClassDeclaration(context: ScriptEnhanceContext, node: tree.Node) !void { const name_node = tree.childByField(node, "name") orelse return; const raw_name = tree.slice(context.source, name_node); const expected_name = try std.fmt.allocPrint(context.scratch, "{s}.{s}", .{ context.module, raw_name }); const known_class = graph_mod.getNode(context.graph, expected_name) != null; const class_name = try enhanceClassNode(context.allocator, context.scratch, context.graph, context.module, context.path, context.source, node, raw_name, context.stats); if (!known_class) try addHeritageEdges(context.allocator, context.stats, class_name, tree.slice(context.source, node)); try enhanceClassBody(context.allocator, context.scratch, context.graph, class_name, context.path, context.source, node, context.stats);}fn enhanceNamedInterface(context: ScriptEnhanceContext, node: tree.Node) !void { const name_node = tree.childByField(node, "name") orelse return; try enhanceInterfaceNode(context.allocator, context.scratch, context.graph, context.module, context.path, context.source, node, tree.slice(context.source, name_node), context.stats);}fn enhanceNamedFunction(context: ScriptEnhanceContext, node: tree.Node) !void { const name_node = tree.childByField(node, "name") orelse return; try enhanceFunctionNode(context.allocator, context.scratch, context.graph, context.module, context.path, context.source, node, tree.slice(context.source, name_node), context.stats);}fn enhanceVariableChild(context: ScriptEnhanceContext, node_kind: []const u8, node: tree.Node) !bool { if (!std.mem.eql(u8, node_kind, "lexical_declaration") and !std.mem.eql(u8, node_kind, "variable_declaration")) return false; try enhanceLexical(context.allocator, context.scratch, context.graph, context.module, context.path, context.source, node, context.stats); return true;}fn enhanceDefaultFunctionChild(context: ScriptEnhanceContext, node_kind: []const u8, node: tree.Node) !void { if (!std.mem.eql(u8, node_kind, "arrow_function") and !std.mem.eql(u8, node_kind, "function_expression")) return; try enhanceFunctionNode(context.allocator, context.scratch, context.graph, context.module, context.path, context.source, node, "default", context.stats);}fn enhanceImport(allocator: std.mem.Allocator, scratch: std.mem.Allocator, graph: *graph_mod.Graph, module: []const u8, path: []const u8, source: []const u8, node: tree.Node, stats: *Stats) !void { const children = tree.childCount(node); for (0..children) |raw_index| { const child_node = tree.child(node, @intCast(raw_index)); if (!std.mem.eql(u8, tree.kind(child_node), "string")) continue; const raw = core.quoted(tree.slice(source, child_node)) orelse return; const target = try importTarget(allocator, scratch, raw, module, path) orelse return; if (core.edgeExists(graph.*, module, model.RelType.imports, target) or core.deferredEdgeExists(stats.*, module, model.RelType.imports, target)) return; try core.addImportEdge(allocator, graph, module, target, &.{}, stats); return; }}fn enhanceClassNode(allocator: std.mem.Allocator, scratch: std.mem.Allocator, graph: *graph_mod.Graph, module: []const u8, path: []const u8, source: []const u8, node: tree.Node, name: []const u8, stats: *Stats) ![]const u8 { const qualified = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ module, name }); try upsertNamedNodeFromTree(allocator, graph, module, qualified, model.NodeType.class, path, tree.startLine(node), tree.endLine(node), try scriptSpanMetadata(allocator, scratch, source, node), stats); return qualified;}fn enhanceInterfaceNode(allocator: std.mem.Allocator, scratch: std.mem.Allocator, graph: *graph_mod.Graph, module: []const u8, path: []const u8, source: []const u8, node: tree.Node, name: []const u8, stats: *Stats) !void { const qualified = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ module, name }); try upsertNamedNodeFromTree(allocator, graph, module, qualified, model.NodeType.interface, path, tree.startLine(node), tree.endLine(node), try scriptSpanMetadata(allocator, scratch, source, node), stats);}fn enhanceClassBody(allocator: std.mem.Allocator, scratch: std.mem.Allocator, graph: *graph_mod.Graph, class_name: []const u8, path: []const u8, source: []const u8, node: tree.Node, stats: *Stats) !void { const body = tree.childByField(node, "body") orelse core.findChild(node, "class_body") orelse return; const children = tree.childCount(body); for (0..children) |raw_index| { const child_node = tree.child(body, @intCast(raw_index)); if (!std.mem.eql(u8, tree.kind(child_node), "method_definition")) continue; const name_node = tree.childByField(child_node, "name") orelse core.findChild(child_node, "property_identifier") orelse core.findChild(child_node, "identifier") orelse continue; try enhanceMethodNode(allocator, scratch, graph, class_name, path, source, child_node, tree.slice(source, name_node), stats); }}fn enhanceMethodNode(allocator: std.mem.Allocator, scratch: std.mem.Allocator, graph: *graph_mod.Graph, class_name: []const u8, path: []const u8, source: []const u8, node: tree.Node, name: []const u8, stats: *Stats) !void { const qualified = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ class_name, name }); try upsertNamedNodeFromTree(allocator, graph, class_name, qualified, model.NodeType.method, path, tree.startLine(node), tree.endLine(node), try javascriptFunctionMetadata(allocator, scratch, stats.metricInputAllocator(allocator), source, node), stats); try extractTreeCalls(allocator, source, qualified, node, stats, class_name);}fn enhanceLexical(allocator: std.mem.Allocator, scratch: std.mem.Allocator, graph: *graph_mod.Graph, module: []const u8, path: []const u8, source: []const u8, node: tree.Node, stats: *Stats) !void { const children = tree.childCount(node); for (0..children) |raw_index| { const child_node = tree.child(node, @intCast(raw_index)); if (!std.mem.eql(u8, tree.kind(child_node), "variable_declarator")) continue; const name_node = tree.childByField(child_node, "name") orelse continue; if (!std.mem.eql(u8, tree.kind(name_node), "identifier")) continue; const value_node = tree.childByField(child_node, "value") orelse continue; const value_kind = tree.kind(value_node); if (!std.mem.eql(u8, value_kind, "arrow_function") and !std.mem.eql(u8, value_kind, "function_expression")) continue; try enhanceFunctionNode(allocator, scratch, graph, module, path, source, value_node, tree.slice(source, name_node), stats); }}fn enhanceFunctionNode(allocator: std.mem.Allocator, scratch: std.mem.Allocator, graph: *graph_mod.Graph, module: []const u8, path: []const u8, source: []const u8, node: tree.Node, name: []const u8, stats: *Stats) !void { const qualified = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ module, name }); try upsertNamedNodeFromTree(allocator, graph, module, qualified, model.NodeType.function, path, tree.startLine(node), tree.endLine(node), try javascriptFunctionMetadata(allocator, scratch, stats.metricInputAllocator(allocator), source, node), stats); try extractTreeCalls(allocator, source, qualified, node, stats, null);}fn upsertNamedNodeFromTree(allocator: std.mem.Allocator, graph: *graph_mod.Graph, parent: []const u8, name: []const u8, node_type: []const u8, path: []const u8, line: i64, end_line: i64, metadata: []const model.Pair, stats: *Stats) !void { const exists = graph.node_index.contains(name); try graph_mod.addNode(graph, .{ .name = name, .type = node_type, .file = path, .line = line, .end_line = end_line, .metadata = metadata }); if (exists) return; try core.addScanEdge(graph, stats, .{ .source = parent, .rel = model.RelType.contains, .target = name, .metadata = try model.sourcePair(allocator, "scan") }); stats.nodes_added += 1; try core.countType(stats, allocator, node_type);}fn extractTreeCalls(allocator: std.mem.Allocator, source: []const u8, caller: []const u8, node: tree.Node, stats: *Stats, class_name: ?[]const u8) !void { const body = tree.childByField(node, "body") orelse core.findChild(node, "statement_block") orelse return; try extractTreeCallsFromNode(allocator, source, caller, body, stats, class_name);}fn extractTreeCallsFromNode(allocator: std.mem.Allocator, source: []const u8, caller: []const u8, node: tree.Node, stats: *Stats, class_name: ?[]const u8) !void { const node_kind = tree.kind(node); if (std.mem.eql(u8, node_kind, "new_expression")) return; if (std.mem.eql(u8, node_kind, "call_expression")) { const target = try treeCallTarget(allocator, source, node, class_name) orelse return; if (!core.deferredEdgeExists(stats.*, caller, model.RelType.calls, target.name)) try stats.deferred.append(.{ .source = caller, .rel = model.RelType.calls, .target = target.name }); return; } const children = tree.childCount(node); for (0..children) |raw_index| try extractTreeCallsFromNode(allocator, source, caller, tree.child(node, @intCast(raw_index)), stats, class_name);}fn treeCallTarget(allocator: std.mem.Allocator, source: []const u8, node: tree.Node, class_name: ?[]const u8) !?CallTarget { const target_node = tree.childByField(node, "function") orelse core.namedChildByIndex(node, 0) orelse return null; const target_kind = tree.kind(target_node); if (std.mem.eql(u8, target_kind, "identifier")) { const raw = tree.slice(source, target_node); if (builtin(raw) or control(raw)) return null; return .{ .name = try allocator.dupe(u8, raw), .resolved = false }; } if (!std.mem.eql(u8, target_kind, "member_expression")) return null; const object_node = tree.childByField(target_node, "object") orelse core.namedChildByIndex(target_node, 0) orelse return null; const property_node = tree.childByField(target_node, "property") orelse core.namedChildByIndex(target_node, 1) orelse return null; const object_kind = tree.kind(object_node); const property_kind = tree.kind(property_node); if ((!std.mem.eql(u8, object_kind, "identifier") and !std.mem.eql(u8, object_kind, "member_expression") and !std.mem.eql(u8, object_kind, "this") and !std.mem.eql(u8, object_kind, "super")) or (!std.mem.eql(u8, property_kind, "identifier") and !std.mem.eql(u8, property_kind, "property_identifier") and !std.mem.eql(u8, property_kind, "private_property_identifier"))) return null; const object = tree.slice(source, object_node); const property = tree.slice(source, property_node); if (builtin(property) or control(property)) return null; if (std.mem.eql(u8, object, "this") and class_name != null) return .{ .name = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ class_name.?, property }), .resolved = true }; if (std.mem.eql(u8, object, "super") or builtin(object)) return null; return .{ .name = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ object, property }), .resolved = false };}fn functionBody(trimmed: []const u8) ?ScriptBody { if (variableDeclaration(trimmed)) |decl| { const value = text.trim(decl.value); if (std.mem.indexOf(u8, value, "=>")) |arrow| { const body = text.trim(value[arrow + "=>".len ..]); if (std.mem.startsWith(u8, body, "{")) return .{ .fragment = body[1..], .braced = true }; return .{ .fragment = body, .braced = false }; } if (std.mem.startsWith(u8, value, "function")) return bracedBody(value); } if (defaultFunctionValue(trimmed)) |value| { if (std.mem.indexOf(u8, value, "=>")) |arrow| { const body = text.trim(value[arrow + "=>".len ..]); if (std.mem.startsWith(u8, body, "{")) return .{ .fragment = body[1..], .braced = true }; return .{ .fragment = body, .braced = false }; } if (std.mem.startsWith(u8, value, "function")) return bracedBody(value); } return bracedBody(trimmed);}fn bracedBody(trimmed: []const u8) ?ScriptBody { const open = std.mem.indexOfScalar(u8, trimmed, '{') orelse return null; return .{ .fragment = trimmed[open + 1 ..], .braced = true };}fn extractCallsOutsideNestedArrows(allocator: std.mem.Allocator, caller: []const u8, fragment: []const u8, depth_before: i64, paren_depth_before: i64, stats: *Stats, class_name: ?[]const u8) !?ScriptArrowSkip { if (std.mem.indexOf(u8, fragment, "=>")) |arrow| { try extractCalls(allocator, caller, fragment[0..arrow], stats, class_name); const body = text.trim(fragment[arrow + "=>".len ..]); if (std.mem.startsWith(u8, body, "{")) return .{ .kind = .brace, .brace_depth = depth_before, .paren_depth = paren_depth_before }; if (body.len == 0 or parenDelta(fragment) > 0) return .{ .kind = .paren, .brace_depth = depth_before, .paren_depth = paren_depth_before }; return null; } try extractCalls(allocator, caller, fragment, stats, class_name); return null;}fn extractCalls(allocator: std.mem.Allocator, caller: []const u8, fragment: []const u8, stats: *Stats, class_name: ?[]const u8) !void { if (std.mem.startsWith(u8, text.trim(fragment), "//")) return; var offset: usize = 0; while (offset < fragment.len) { const paren_rel = std.mem.indexOfScalar(u8, fragment[offset..], '(') orelse break; const paren = offset + paren_rel; offset = paren + 1; const target = try callTarget(allocator, fragment, paren, class_name) orelse continue; try stats.deferred.append(.{ .source = caller, .rel = model.RelType.calls, .target = target.name }); }}fn callTarget(allocator: std.mem.Allocator, fragment: []const u8, paren: usize, class_name: ?[]const u8) !?CallTarget { if (paren == 0) return null; var end = paren; while (end > 0 and std.ascii.isWhitespace(fragment[end - 1])) end -= 1; var start = end; while (start > 0 and jsIdentByte(fragment[start - 1])) start -= 1; if (start == end) return null; var before = start; while (before > 0 and std.ascii.isWhitespace(fragment[before - 1])) before -= 1; const raw = fragment[start..end]; if (builtin(raw) or control(raw)) return null; if (before > 0 and fragment[before - 1] == '.') { const object = memberObject(fragment, before - 1) orelse return null; if (precededByNew(fragment, object.start)) return null; if (std.mem.eql(u8, object.value, "this") and class_name != null) return .{ .name = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ class_name.?, raw }), .resolved = true }; if (std.mem.eql(u8, object.value, "super") or builtin(object.value)) return null; return .{ .name = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ object.value, raw }), .resolved = false }; } if (precededByNew(fragment, start)) return null; return .{ .name = try allocator.dupe(u8, raw), .resolved = false };}fn memberObject(fragment: []const u8, dot: usize) ?ScriptMemberObject { if (dot == 0) return null; var end = dot; while (end > 0 and std.ascii.isWhitespace(fragment[end - 1])) end -= 1; var start = end; while (start > 0 and (jsIdentByte(fragment[start - 1]) or fragment[start - 1] == '.')) start -= 1; if (start == end) return null; return .{ .value = fragment[start..end], .start = start };}fn precededByNew(fragment: []const u8, callee_start: usize) bool { var end = callee_start; while (end > 0 and std.ascii.isWhitespace(fragment[end - 1])) end -= 1; var start = end; while (start > 0 and jsIdentByte(fragment[start - 1])) start -= 1; return start != end and std.mem.eql(u8, fragment[start..end], "new");}fn control(name: []const u8) bool { const controls = [_][]const u8{ "if", "for", "while", "switch", "catch", "function", "class", "return", "throw", "await", "new", "typeof", "delete", "void", "super", "of", "in" }; for (controls) |item| if (std.mem.eql(u8, name, item)) return true; return false;}fn builtin(name: []const u8) bool { const builtins = [_][]const u8{ "console", "require", "setTimeout", "Promise", "JSON", "Math", "Object", "Array", "String", "Number", "Boolean", "Date", "RegExp", "Error", "Map", "Set", "WeakMap", "WeakSet", "Symbol", "Proxy", "Reflect", "parseInt", "parseFloat", "isNaN", "isFinite", "encodeURI", "encodeURIComponent", "decodeURI", "decodeURIComponent", "fetch", "alert", "confirm", "prompt", "TypeError", "RangeError", "ReferenceError", "SyntaxError" }; for (builtins) |item| if (std.mem.eql(u8, name, item)) return true; return false;}fn jsImport(trimmed: []const u8) ?[]const u8 { if (std.mem.startsWith(u8, trimmed, "import ")) { if (std.mem.indexOf(u8, trimmed, " from ")) |from| { const rest = text.trim(trimmed[from + " from ".len ..]); return core.quoted(rest); } return core.quoted(text.trim(trimmed["import ".len..])); } if (std.mem.startsWith(u8, trimmed, "require(")) return core.quoted(trimmed["require(".len..]); return null;}fn importTarget(allocator: std.mem.Allocator, scratch: std.mem.Allocator, raw: []const u8, module: []const u8, path: []const u8) !?[]const u8 { const stripped = scan_files.removeKnownExtension(raw); if (std.mem.startsWith(u8, stripped, ".")) return try relativeImportTarget(allocator, scratch, stripped, module, path); return try bareImportTarget(allocator, scratch, stripped);}fn bareImportTarget(allocator: std.mem.Allocator, scratch: std.mem.Allocator, raw: []const u8) !?[]const u8 { var value = text.trim(raw); if (value.len != 0 and value[0] == '@') value = value[1..]; var parts: std.ArrayList([]const u8) = .empty; var split = std.mem.splitScalar(u8, value, '/'); while (split.next()) |part| { const item = text.trim(part); if (item.len == 0 or std.mem.eql(u8, item, ".")) continue; try parts.append(scratch, item); } if (parts.items.len != 0 and std.mem.eql(u8, parts.items[parts.items.len - 1], "index")) _ = parts.pop(); if (parts.items.len == 0) return null; return try core.joinDotted(allocator, parts.items);}fn relativeImportTarget(allocator: std.mem.Allocator, scratch: std.mem.Allocator, raw: []const u8, module: []const u8, path: []const u8) !?[]const u8 { var resolved: std.ArrayList([]const u8) = .empty; var module_split = std.mem.splitScalar(u8, module, '.'); while (module_split.next()) |part| try resolved.append(scratch, part); const stem = scan_files.removeKnownExtension(text.basename(path)); if (!std.mem.eql(u8, stem, "index") and !std.mem.eql(u8, stem, "__init__") and resolved.items.len != 0) _ = resolved.pop(); var path_split = std.mem.splitScalar(u8, raw, '/'); while (path_split.next()) |part| { const item = text.trim(part); if (item.len == 0 or std.mem.eql(u8, item, ".")) continue; if (std.mem.eql(u8, item, "..")) { if (resolved.items.len == 0) return null; _ = resolved.pop(); continue; } try resolved.append(scratch, item); } if (resolved.items.len != 0 and std.mem.eql(u8, resolved.items[resolved.items.len - 1], "index")) _ = resolved.pop(); if (resolved.items.len == 0) return null; return try core.joinDotted(allocator, resolved.items);}fn className(trimmed: []const u8) ?[]const u8 { if (prefixedName(trimmed, "class ")) |name| return name; if (prefixedName(trimmed, "export class ")) |name| return name; if (prefixedName(trimmed, "export default class ")) |name| return name; if (prefixedName(trimmed, "abstract class ")) |name| return name; if (prefixedName(trimmed, "export abstract class ")) |name| return name; return null;}fn interfaceName(trimmed: []const u8) ?[]const u8 { return prefixedName(trimmed, "interface ");}fn addHeritageEdges(allocator: std.mem.Allocator, stats: *Stats, class_name: []const u8, trimmed: []const u8) !void { try addHeritageEdgesFor(allocator, stats, class_name, trimmed, "extends", model.RelType.inherits); try addHeritageEdgesFor(allocator, stats, class_name, trimmed, "implements", model.RelType.implements);}fn addHeritageEdgesFor(allocator: std.mem.Allocator, stats: *Stats, class_name: []const u8, trimmed: []const u8, keyword: []const u8, rel: []const u8) !void { const start = std.mem.indexOf(u8, trimmed, keyword) orelse return; var rest = text.trim(trimmed[start + keyword.len ..]); const brace = std.mem.indexOfScalar(u8, rest, '{') orelse rest.len; rest = text.trim(rest[0..brace]); var split = std.mem.splitScalar(u8, rest, ','); while (split.next()) |raw| { const target = heritageTarget(raw) orelse continue; try stats.deferred.append(.{ .source = class_name, .rel = rel, .target = try allocator.dupe(u8, target) }); }}fn heritageTarget(raw: []const u8) ?[]const u8 { const trimmed = text.trim(raw); var end: usize = 0; while (end < trimmed.len and (jsIdentByte(trimmed[end]) or trimmed[end] == '.')) end += 1; if (end == 0) return null; return trimmed[0..end];}fn functionName(trimmed: []const u8) ?[]const u8 { if (prefixedName(trimmed, "function ")) |name| return name; if (prefixedName(trimmed, "async function ")) |name| return name; if (prefixedName(trimmed, "export function ")) |name| return name; if (prefixedName(trimmed, "export async function ")) |name| return name; if (prefixedName(trimmed, "export default function ")) |name| return name; if (defaultFunctionValue(trimmed) != null) return "default"; const decl = variableDeclaration(trimmed) orelse return null; const value = text.trim(decl.value); if (std.mem.startsWith(u8, value, "function") or arrowValue(value)) return decl.name; return null;}fn defaultFunctionValue(trimmed: []const u8) ?[]const u8 { if (!std.mem.startsWith(u8, trimmed, "export default ")) return null; const value = text.trim(trimmed["export default ".len..]); if (std.mem.startsWith(u8, value, "function") or arrowValue(value)) return value; return null;}fn prefixedName(trimmed: []const u8, prefix: []const u8) ?[]const u8 { if (!std.mem.startsWith(u8, trimmed, prefix)) return null; const rest = trimmed[prefix.len..]; var end: usize = 0; while (end < rest.len and jsIdentByte(rest[end])) end += 1; if (end == 0) return null; return rest[0..end];}fn variableDeclaration(trimmed: []const u8) ?ScriptDecl { var rest: []const u8 = undefined; var value = trimmed; if (std.mem.startsWith(u8, value, "export ")) value = text.trim(value["export ".len..]); if (std.mem.startsWith(u8, value, "const ")) { rest = value["const ".len..]; } else if (std.mem.startsWith(u8, value, "let ")) { rest = value["let ".len..]; } else if (std.mem.startsWith(u8, value, "var ")) { rest = value["var ".len..]; } else { return null; } var end: usize = 0; while (end < rest.len and jsIdentByte(rest[end])) end += 1; if (end == 0) return null; const after_name = text.trim(rest[end..]); if (!std.mem.startsWith(u8, after_name, "=")) return null; return .{ .name = rest[0..end], .value = after_name[1..] };}fn arrowValue(raw: []const u8) bool { var value = raw; if (std.mem.startsWith(u8, value, "async ")) value = text.trim(value["async ".len..]); if (std.mem.startsWith(u8, value, "((")) return false; const arrow = std.mem.indexOf(u8, value, "=>") orelse return false; const call = std.mem.indexOfScalar(u8, value, '('); if (call != null and call.? < arrow and !std.mem.startsWith(u8, value, "(")) return false; return true;}fn constantName(trimmed: []const u8) ?[]const u8 { const decl = variableDeclaration(trimmed) orelse return null; return if (core.isUpper(decl.name)) decl.name else null;}fn methodName(trimmed: []const u8) ?[]const u8 { if (std.mem.indexOf(u8, trimmed, "=>") != null) return null; const paren = std.mem.indexOfScalar(u8, trimmed, '(') orelse return null; var start = paren; while (start > 0 and jsIdentByte(trimmed[start - 1])) start -= 1; if (start == paren) return null; const name = trimmed[start..paren]; if (control(name)) return null; var prefix = text.trim(trimmed[0..start]); if (prefix.len != 0) { while (true) { const before = prefix; prefix = trimMethodModifier(prefix, "public"); prefix = trimMethodModifier(prefix, "private"); prefix = trimMethodModifier(prefix, "protected"); prefix = trimMethodModifier(prefix, "static"); prefix = trimMethodModifier(prefix, "async"); prefix = trimMethodModifier(prefix, "override"); prefix = trimMethodModifier(prefix, "get"); prefix = trimMethodModifier(prefix, "set"); if (std.mem.eql(u8, before, prefix)) break; } if (prefix.len != 0) return null; } return name;}fn trimMethodModifier(value: []const u8, modifier: []const u8) []const u8 { if (std.mem.eql(u8, value, modifier)) return ""; if (std.mem.startsWith(u8, value, modifier) and value.len > modifier.len and std.ascii.isWhitespace(value[modifier.len])) return text.trim(value[modifier.len..]); return value;}fn parenDelta(line: []const u8) i64 { var delta: i64 = 0; for (line) |byte| { if (byte == '(') delta += 1; if (byte == ')') delta -= 1; } return delta;}fn jsIdentByte(byte: u8) bool { return identByte(byte) or byte == '$';}Audit
| Definitions | 4 |
|---|---|
| Public names | 4 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |