class

Analyzer::Python::FastAPI

Inherits Analyzer::Python::PythonEngine < Analyzer < FileHelper < Reference < Object

Constants

ALIAS_GENERATOR_RE = /\balias_generator\s*=\s*([\w.]+)/
BODY_CONTAINER_TYPE_RE = /\A(?:typing\.)?(?:dict\[|(?:Dict|list|List|set|Set|frozenset|FrozenSet|tuple|Tuple|Sequence)(?:\[|\z))/

A bare dict keeps reading the keys the handler takes from it.

BODY_EMBED_RE = /\bembed\s*=\s*True\b/
CLASS_DEPENDENCY_ATTR_RE = /^\s*#{PYTHON_VAR_NAME_REGEX}\s*(?::[^=]+)?=\s*(?:Depends|Security)\s*\(/
CLASS_HEADER_RE = /^(\s*)class\s+(#{PYTHON_VAR_NAME_REGEX})\s*(?:\(([^)]*)\))?\s*:/
DEPENDS_CALLEE_RE = /\b(?:Depends|Security)\s*\(\s*(?:dependency\s*=\s*)?(#{PYTHON_VAR_NAME_REGEX}(?:\.#{PYTHON_VAR_NAME_REGEX})?)\s*[,)]/
DEPENDS_CLASS_RE = /\bDepends\s*\(\s*([A-Z]\w*)\s*\)/
DEPENDS_EMPTY_RE = /\bDepends\s*\(\s*\)/
FASTAPI_INSTANCE_RE = /(#{PYTHON_VAR_NAME_REGEX})(?::#{DOT_NATION})?=(?:fastapi\.)?FastAPI\(/

Constant-only matchers hoisted out of the per-line analyze loops so they aren't recompiled (Crystal rebuilds an interpolated regex literal on every evaluation). The .to_s expansion of the interpolated constants is byte-identical to the previous inline form.

FASTAPI_PARAM_CLASS_RE = /\b(?:Header|Query|Cookie|Path|Body|Form|File)\s*\(/

The Python identifier a FastAPI handler declares is not always the name the client sends. Two rules rewrite it, and Noir honored neither:

  • alias= overrides the name outright, for every parameter class (Header, Query, Cookie, Form, File, Body, Path).
  • Header — and only Header — additionally maps _ to -, because an HTTP header name can't carry an underscore in most stacks. That is convert_underscores, and it defaults to True; passing convert_underscores=False is what opts back out.

Reporting the identifier verbatim is not a cosmetic mismatch: it is the name Noir hands to the next stage. -f curl emitted -H 'x_token: ' for x_token: str = Header(None), a header the app reads as nothing, and a DAST tool importing the OpenAPI doc probed ?q_name= for a parameter the app only accepts as q-name.

FASTAPI_PARAM_RE = /(?:^|[(,])(#{PYTHON_VAR_NAME_REGEX}):(?:fastapi\.)?FastAPI(?:[,)=]|$)/

def init_views(app: FastAPI) (space-stripped): the app built elsewhere and handed in; its include_router / @app.get calls in this file register on it too.

HANDLER_CALL_RE = /^(#{PYTHON_VAR_NAME_REGEX}(?:\.#{PYTHON_VAR_NAME_REGEX})?)\s*\(/
HANDLER_REF_RE = /^#{PYTHON_VAR_NAME_REGEX}(?:\.#{PYTHON_VAR_NAME_REGEX})?$/
HELPER_DEF = /^(\s*)(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(/
INCLUDE_PARAM = /\.\s*include_router\s*\(\s*([A-Za-z_]\w*)\s*[,)]/
INIT_DEF_RE = /^\s+(?:async\s+)?def\s+__init__\s*\(/
LIBRARY_ROUTER_CLASSES = ["APIRouter", "VersionedAPIRouter", "InferringRouter"] of ::String

Library subclasses of fastapi.APIRouter, constructed like it.

LOCATION_MARKER_RE = /\b(?:Query|Header|Cookie|Body|Form|File|UploadFile|Path|WebSocket|Depends|Security)\b/
MODEL_FIELD_CALL_RE = /\bField\s*\(/

The same rename, one layer down. A request body is described by a Pydantic model, and Field(alias=...) renames the key there too — with populate_by_name off (the default) the alias is the only key the model accepts, so the attribute name is a body field the app rejects outright.

MODEL_FIELD_LINE_RE = /^[A-Za-z]\w*\s*:/
MODEL_TYPE_WRAPPER_RE = /^(?:typing\.)?(Optional|Annotated|Union)\[(.*)\]$/m
NO_CONVERT_UNDERSCORES_RE = /\bconvert_underscores\s*=\s*False\b/
PATH_PARAM_REGEX = /\{(#{PYTHON_VAR_NAME_REGEX})(?::[a-zA-Z_][a-zA-Z0-9_]*)?\}/
QUALIFIED_NAME_RE = /^#{PYTHON_VAR_NAME_REGEX}(?:\.#{PYTHON_VAR_NAME_REGEX})+$/
REQUEST_ATTR_TYPES = {"query_params" => "query", "headers" => "header", "cookies" => "cookie"}
ROUTER_SUBCLASS_RE = /^class\s+(#{PYTHON_VAR_NAME_REGEX})\s*\(([^)]*)\)\s*:/m

class UserRouter(APIRouter): — a project router class, constructed like APIRouter itself.

SCALAR_TYPE_RE = /\b(?:str|int|float|bool|EmailStr)\b/
STAR_IMPORT_RE = /^from\s+([\w.]+)\s+import\s+\*/m
TYPED_PATH_PARAM_REGEX = /\{(#{PYTHON_VAR_NAME_REGEX}):[a-zA-Z_][a-zA-Z0-9_]*\}/
VAR_NAME_FULL_RE = /^#{PYTHON_VAR_NAME_REGEX}$/

Class methods

tech_name
Source

Instance methods

analyze
Source
configure_router_prefix(file : String, include_router_map : Hash(String, Hash(String, Router)), app_base_path : String, router_prefix : String = "", target_instance_name : String | Nil = nil, visited : Set(String) = Set(::String).new)

Configures the prefix for each router

Source
find_base_model_params(source : String, class_name : String, param_name : String, visited : Set(String) = Set(::String).new, path : String | Nil = nil, base_path : String | Nil = nil, location : String = "json") : Array(Param)

Finds the parameters for a base model class, base classes first (Pydantic orders inherited fields that way): those declared in the same source, else, given path, where the source imports them from. Field names follow an inherited alias_generator named for camelCase unless a Field(alias=...) names the field itself.

Source
find_dictionary_params(source : String, param : FunctionParameter) : Array(Param)

Finds parameters in dictionary structures

Source
infer_parameter_type(data : String, is_param_type = false) : String | Nil

Infers the type of the parameter based on its default value or type annotation

Source
resolve_constant_value(expression : String, import_modules : Hash(String, Tuple(String, Int32))) : String | Nil

Best-effort resolver for a non-literal prefix= expression in include_router(...). Handles two shapes the wild produces:

  • prefix=API_V1_STR — a top-level constant imported via from app.core.config import API_V1_STR. We look up the constant's defining module via import_modules and search it for NAME = "literal".
  • prefix=settings.API_V1_STR — class-attribute access on an imported instance. We look up settings's module and search it for API_V1_STR: str = "literal" (the BaseSettings shape full-stack-fastapi-template uses) or API_V1_STR = "literal".

Returns nil when the expression doesn't match either shape or we can't find the underlying file — the caller then falls back to an empty prefix so we never surface raw Python expressions in URLs.

Source
tech

Instance-side view of the same declaration. The per-file rescues live on this base class, which has no way to name the analyzer that is running inside them, so a skipped file could not be attributed to a tech. Deriving it from analyzer_for keeps the name written exactly once.

Source