module Analyzer::Dart::Helper

Overview

Shared helpers for the Dart framework analyzers (Dart Frog, Shelf, Serverpod). Kept framework-agnostic so each analyzer can opt in without duplicating path/string conventions.

Extended Modules

Defined in:

analyzer/analyzers/dart/dart_helper.cr

Constant Summary

HANDLER_REFERENCE_REGEX = /\A[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*\z/

Matches a bare handler reference (_createUser, auth.handler) passed as a route's handler argument.

SPLIT_ARGS_RULES = Noir::TopLevelSplit::Rules.new(nest: ((Noir::TopLevelSplit::Nest::Paren | Noir::TopLevelSplit::Nest::Bracket) | Noir::TopLevelSplit::Nest::Brace) | Noir::TopLevelSplit::Nest::Angle, quotes: "\"'", escape: Noir::TopLevelSplit::Escape::InQuotes, strip: false, empties: Noir::TopLevelSplit::Empties::Keep, per_kind: true, clamp: true)

Each bracket kind ((), {}, [], <>) gets its own counter so a generic argument (Map<String, int>) and a collection literal ([a, b]) both keep their inner commas, and string literals are skipped so a comma inside 'a,b' never splits. Nothing is stripped and every part is kept, the trailing one included, so a single-argument call yields a one-element array.

File-local rather than a Rules preset: it is the only splitter in the tree that counts <> and also keeps every part unstripped, so a central name would have exactly one user.

Instance Method Summary

Instance Method Detail

def build_endpoint(url : String, verb : String, path : String, line : Int32, callees : Array(Noir::DartCalleeExtractor::Entry)) : Endpoint #

Build an endpoint from an already-normalized URL, promoting every {name} capture in it to a path param and attaching the callees. Analyzers whose endpoints carry extra state (Dart Frog's websocket flag, dart:io HttpServer's inherited params) keep their own.


[View source]
def extract_string_literal(text : String) : String | Nil #

Pull the contents of a leading single/double-quoted string literal from an argument expression, honouring backslash escapes. Returns nil when the expression doesn't start with a string literal.


[View source]
def find_matching_paren(text : String, open_idx : Int32) : Int32 | Nil #

Index of the ) closing the ( at open_idx, or nil when the expression never balances. String literals are skipped so a paren inside 'a)b' doesn't close the call, and backslash escapes inside them are honoured.

Both the argument and the result stay in CHAR space, consistent with Regex::MatchData#begin, Analyzer#line_number_for_index and the char_index_to_byte_index conversions the analyzers use for callee extraction.


[View source]
def first_top_level_comma(text : String, start : Int32, limit : Int32) : Int32 | Nil #

Char index of the first comma at paren/brace/bracket depth zero between start and limit, or nil when the call has a single argument.


[View source]
def handler_callees(handler_arg : String, content : String, handler_start : Int32, path : String, line : Int32) : Array(Noir::DartCalleeExtractor::Entry) #

Callees for a route's handler argument, where handler_start is the CHAR index at which the handler expression begins (typically the char just past the first top-level comma).

A plain function reference (_createUser, auth.handler) can't be resolved cross-file yet, so the reference itself is recorded as the callee.

extract_body_after scans by BYTE offset, hence the char-to-byte conversion. It skips past the leading (req, res) lambda params to the =>/{ body, so a trailing middleware: argument after the body is naturally excluded.

Shelf keeps its own variant: it derives the handler start from the call's closing paren instead of a comma index, so the two are not interchangeable.


[View source]
def split_top_level_args(text : String) : Array(String) #

Split a call's argument list on top-level commas.

Note: Serverpod#split_top_level_commas is deliberately NOT this method — it tracks only </( on one shared counter and is not string-aware, matching the narrower shapes it parses. It goes through the same shared splitter with its own Rules, so the difference is now two constants to compare rather than two loops to diff.


[View source]
def strip_comments(text : String) : String #

Replace // line and /* */ block comments with spaces, leaving string literals and overall byte offsets intact so downstream regex/offset logic still lines up with the original source.


[View source]
def test_path?(path : String, base_path : String | Nil = nil) : Bool #

Standard Dart test-file conventions. The Dart tooling discovers tests under a project-root test/ directory and via the *_test.dart suffix; neither ever serves real traffic. Dart Frog in particular mirrors the route tree under test/routes/, so a naive /routes/ match would surface every mock handler as a live endpoint. Centralized so every Dart analyzer can opt in via next if Helper.test_path?(path, base_paths).

  • /test/, test/ — Dart's dart test discovery root and the Dart Frog test/routes/ mirror tree
  • *_test.dart — the canonical Dart unit-test suffix

[View source]
def test_path?(path : String, base_paths : Array(String)) : Bool #

[View source]