class Analyzer::Python::Flask

Defined in:

analyzer/analyzers/python/flask.cr

Constant Summary

ADD_RESOURCE_RE = /(#{PYTHON_VAR_NAME_REGEX})\s*\.\s*add_resource\s*\((.+)\)/m

flask_restful / flask-restx register class-based Resources with api.add_resource(ResourceClass, "/url"[, "/url2"], endpoint=...). Each Resource exposes one endpoint per HTTP-verb method it defines. Hoisted for the common (alias-free) case so the per-file regex isn't recompiled; ADD_RESOURCE_SUBSTRING is the cheap per-line guard.

ADD_RESOURCE_SUBSTRINGS = [".add_resource("]
ADD_URL_RULE_RE = /(#{PYTHON_VAR_NAME_REGEX})\.add_url_rule\((.+)\)/m
COMPETING_FRAMEWORK_IMPORT_RE = /^\s*(?:from|import)\s+(?:fastapi|sanic|litestar|starlette|quart|robyn|ninja|bottle|aiohttp|falcon|pyramid|tornado)\b/m

Decorator-based Python web frameworks whose route syntax is indistinguishable from Flask's at the line level — @app.get("/x"), @api.post("/x"). A file that imports one of these and never mentions flask belongs to that framework's analyzer. ninja (Django Ninja), bottle and aiohttp were missing from the list, so the Flask analyzer claimed their handler files and relabelled the routes python_flask with Flask-shaped (usually empty) params.

DOTTED_REFERENCE_RE = /^#{DOT_NATION}$/
FLASK_INSTANCE_RE = /(#{PYTHON_VAR_NAME_REGEX})(?::#{DOT_NATION})?=(?:flask\.)?Flask\(/

Per-line route-discovery patterns. These interpolate only the PYTHON_VAR_NAME_REGEX/DOT_NATION constants, so the inline literals were recompiling identical PCRE2 patterns on every source line of every file (#analyze runs them per line). Compile once here; the .to_s expansion of the interpolated constants is byte-identical to the previous inline form, so matching behaviour is unchanged.

INIT_APP_RE = /(#{PYTHON_VAR_NAME_REGEX})\.init_app\((#{PYTHON_VAR_NAME_REGEX})/
REGISTER_BLUEPRINT_RE = /(#{PYTHON_VAR_NAME_REGEX})\.register_blueprint\((#{DOT_NATION})/
REQUEST_PARAM_FIELD_PATTERNS = REQUEST_PARAM_FIELDS.map do |field_name, tuple| {tuple[1], Regex.new("request\\.#{field_name}\\[[rf]?['\"]([^'\"]*)['\"]\\]"), Regex.new("request\\.#{field_name}\\.get(?:list)?\\([rf]?['\"]([^'\"]*)['\"]")} end

extract_request_params runs once per route and used to rebuild two PCRE2 patterns per request field on every call (8 fields × 2 = 16 regex compilations per endpoint). PCRE2 JIT-compilation of an interpolated regex literal is ~3µs and dominated Flask scan time (profiling: ~50% of the analyzer). The field names are a fixed set, so precompile the access patterns once here and reuse them. Tuple shape: {noir_param_type, bracket_access_regex, get_access_regex} The get-access pattern accepts .get( and .getlist(: Werkzeug's request MultiDicts (request.args, request.form, …) expose getlist("key") as the standard accessor for repeated keys (?tag=a&tag=b), and it reads the same first string argument as the key — so .get(?:list)? captures both without a separate pattern.

REQUEST_PARAM_FIELDS = {"data" => {["POST", "PUT", "PATCH", "DELETE"], "form"}, "args" => {["GET"], "query"}, "form" => {["POST", "PUT", "PATCH", "DELETE"], "form"}, "files" => {["POST", "PUT", "PATCH", "DELETE"], "form"}, "values" => {["GET", "POST", "PUT", "PATCH", "DELETE"], "query"}, "json" => {["POST", "PUT", "PATCH", "DELETE"], "json"}, "cookies" => {nil, "cookie"}, "headers" => {nil, "header"}}

Reference: https://stackoverflow.com/a/16664376 Reference: https://tedboy.github.io/flask/generated/generated/flask.Request.html

REQUEST_PARAM_TYPES = {"query" => nil, "form" => ["POST", "PUT", "PATCH", "DELETE"], "json" => ["POST", "PUT", "PATCH", "DELETE"], "cookie" => nil, "header" => nil}
RESOURCE_REGISTRAR_DEF_RE = /^(\s*)(?:async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*self\s*,\s*resource\b/

def add_x(self, resource, ...) head of an Api-subclass method that may wrap add_resource (e.g. redash's add_org_resource).

ROUTE_DECORATOR_RE = /^\s*@\s*#{DOT_NATION}\s*\.\s*(?:route|get|post|put|patch|delete|head|options|trace)\s*\(/m

Source-ownership and add_url_rule helpers — same constant-only interpolation (DOT_NATION), hoisted so the per-file/per-call sites don't recompile them. VIEW_FUNC_KWARG_RE has no whitespace tolerance (unlike Quart's) because Flask's add_url_rule args are matched on space-stripped lines.

ROUTE_REGISTRAR_RE = /\b#{DOT_NATION}\s*\.\s*(?:add_url_rule|register_blueprint)\s*\(/
VIEW_ASSIGN_RE = /(#{PYTHON_VAR_NAME_REGEX})(?::#{DOT_NATION})?=(#{PYTHON_VAR_NAME_REGEX})\.as_view\(/
VIEW_FUNC_AS_VIEW_RE = /view_func=(#{PYTHON_VAR_NAME_REGEX})\.as_view\([rf]?['"]([^'"]*)['"]\)/

view_func= extractors for the add_url_rule scan below — same constant-only interpolation, hoisted so they aren't rebuilt on every add_url_rule(...) match found in a file.

VIEW_FUNC_KWARG_RE = /view_func=(#{DOT_NATION})(?:,|\)|$)/
VIEW_FUNC_VAR_RE = /view_func=(#{PYTHON_VAR_NAME_REGEX})[,\)]/

Class Method Summary

Instance Method Summary

Instance methods inherited from class Analyzer::Python::PythonEngine

build_callees_from(body : String, body_start_line : Int32, path : String, *, definition_base_path : String | Nil = nil, source : String | Nil = nil) : Array(Callee) build_callees_from, find_def_line(lines : Array(String), decorator_line : Int32) : Int32 | Nil find_def_line, find_imported_modules(app_base_path : String, file_path : String, content : String | Nil = nil) : Hash(String, Tuple(String, Int32)) find_imported_modules, find_imported_package(package_path : String, dotted_as_names : String) : Array(Tuple(String, String, Int32)) find_imported_package, find_json_params(codeblock_lines : Array(String), json_var_names : Array(String)) : Array(Param) find_json_params, join_until_python_call_closes(lines : Array(String), index : Int32, line : String) : String join_until_python_call_closes, parse_code_block(data : String | Array(String), after : Regex | Nil = nil) : String | Nil parse_code_block, parse_function_def(source_lines : Array(String), start_index : Int32) : FunctionDefinition | Nil parse_function_def, push_callees_from(endpoint : Endpoint, body : String, body_start_line : Int32, path : String, *, definition_base_path : String | Nil = nil, source : String | Nil = nil) : Nil push_callees_from, python_paren_delta(line : String) : Int32 python_paren_delta, python_signature_line_span(lines : Array(String)) : Int32 python_signature_line_span, return_literal_value(data : String) : String return_literal_value

Class methods inherited from class Analyzer::Python::PythonEngine

python_test_path?(path : String, base_path : String | Nil = nil) : Bool python_test_path?

Instance methods inherited from class Analyzer

analyze analyze, base_path : String base_path, base_paths : Array(String) base_paths, base_relative_path(path : String) : String base_relative_path, callees_needed? : Bool callees_needed?, content_matches?(content : String, markers : Regex) : Bool content_matches?, http_header_name(name : String) : String | Nil http_header_name, line_number_for_index(content : String, char_index : Int32) : Int32 line_number_for_index, logger : NoirLogger logger, parallel_analyze(files : Array(String), &block : String -> Nil) parallel_analyze, read_file_content(path : String) : String read_file_content, result : Array(Endpoint) result, unique_params(params : Array(Param)) : Array(Param) unique_params, url : String url, web_root_path(path : String, markers : Array(String)) : String web_root_path

Constructor methods inherited from class Analyzer

new(options : Hash(String, YAML::Any)) new

Macros inherited from class Analyzer

analyzer_for(tech) analyzer_for

Instance methods inherited from module FileHelper

all_files : Array(String) all_files, get_files_by_basename(basename : String) : Array(String) get_files_by_basename, get_files_by_extension(extension : String) : Array(String) get_files_by_extension, get_files_by_extensions(extensions : Array(String)) : Array(String) get_files_by_extensions, get_files_by_prefix(prefix : String) : Array(String) get_files_by_prefix, get_files_by_prefix_and_extension(prefix : String, extension : String) : Array(String) get_files_by_prefix_and_extension, get_files_by_relative_path(relative_path : String, root : String = "") : Array(String) get_files_by_relative_path, get_public_dir_files(base_path : String, folder : String) : Array(String) get_public_dir_files, get_public_files(base_path : String, anchors : Array(String) = ["shard.yml", "Gemfile"]) : Array(String) get_public_files, walked_path(expanded : String) : String walked_path

Class Method Detail

def self.tech_name : String #

[View source]

Instance Method Detail

def analyze #

[View source]
def create_parser(path : String, content : String = "") : PythonParser #

Create a Python parser for a given path and content. The parser walks the file with tree-sitter and recursively absorbs globals from imported modules — no lexer step.


[View source]
def extract_params_from_decorator(path : String, lines : Array(String), line_index : Int32, direction : Symbol = :down) : Tuple(Array(Param), Int32) #

Extracts parameters from the decorator


[View source]
def get_endpoints(method : String, route_path : String, extra_params : String, codeblock_lines : Array(String), prefix : String) #

Extracts endpoint information from the given route and code block


[View source]
def get_filtered_params(method : String, params : Array(Param)) : Array(Param) #

Filters the parameters based on the HTTP method


[View source]
def get_parser(path : String, content : String = "") : PythonParser #

Get a parser for a given path


[View source]