import { TypeShape, WrapperReference, BehavioralSummary } from '@suss/behavioral-ir'; import { Tree, Node } from 'web-tree-sitter'; export { Node as PyNode, Tree as PyTree } from 'web-tree-sitter'; import { Database } from '@suss/datalog'; import { RawCodeStructure, ExtractorOptions, TimingReport, ExtractionReport, CacheDiagnostic } from '@suss/extractor'; import { WhyExplained } from '@suss/resolution'; /** * A new `Parser` for every call, so that parses running at the same time do * not fight over one parser's mutable `language` field. The compiled `Language` * is loaded once per process and shared. */ declare function parsePython(source: string): Promise; /** * Load the grammar ahead of time, for a caller whose own entry point is * async but whose parsing happens later through `parsePythonSync`. Loading * the WASM grammar is the only async part; parsing a tree from it is not. */ declare function preloadPythonGrammar(): Promise; /** * `parsePython` without the await, for a caller that cannot itself be * async. Throws if `preloadPythonGrammar` (or `parsePython`) has not * already loaded the grammar in this process. */ declare function parsePythonSync(source: string): Tree; /** * Maps a dotted import onto the file in the repo it refers to. * * The search walks a small set of configured roots. Finding out what `sys.path` * actually is would mean running Python, so when more than one root offers a * candidate, the resolver abstains rather than picking one. */ interface RelativeModuleSpec { /** The dotted path after the leading dots. It is empty for a bare `from . import x`. */ module: string; /** Dot count: 0 means absolute, 1 is `.`, 2 is `..`, and so on. */ relativeLevel: number; } type ModuleResolution = { status: "resolved"; file: string; } | { status: "unresolved"; /** * "external": no configured root has a file for it. "ambiguous": more * than one does. "outsideRoots": a relative import had enough dots to walk * up past every configured root. */ reason: "external" | "ambiguous" | "outsideRoots"; }; interface ModuleResolverOptions { /** The directories an absolute dotted import is resolved against. We cannot see `sys.path` order, since that only exists at runtime. */ roots: string[]; } declare function resolveAbsoluteModule(dotted: string, options: ModuleResolverOptions): ModuleResolution; /** * The search starts in the importing file's own directory, because Python * resolves a relative import against `__package__` and not against `sys.path`. * `options.roots` only limits how far the dots can walk up, so a deeply relative * import cannot land in an unrelated checkout. */ declare function resolveRelativeModule(importingFile: string, spec: RelativeModuleSpec, options: ModuleResolverOptions): ModuleResolution; declare function resolveModule(importingFile: string, spec: RelativeModuleSpec, options: ModuleResolverOptions): ModuleResolution; /** * The Python adapter's own pattern-pack contract. * * This is deliberately not the TypeScript adapter's `PatternPack`. That type * dispatches its discovery variants through ts-morph-specific handlers, and * Python's two route shapes have no exact match in its union anyway. Match * shapes stay per-language until a second implementation shows what is actually * shared, and this is the per-language one for Python. * * A pack is still plain data, following the same rule the TypeScript packs * follow: it describes what a library defines, never anything a project chose. */ interface PythonPack { name: string; /** * Pack version stamp, which feeds the cache invalidation key. Bump on * any change that affects discovered units or extracted summaries. * The CLI folds a hash of the loaded pack file and its config into * this stamp on top, so a pack run through the CLI invalidates on an * edit whether or not it declares a version of its own. */ version?: string; /** * Files under the project this pack reads that are not among the * `.py` files a run walks, given the files the run is about to walk. * Their content feeds the same cache key the pack's own config does, * so an edit to one of them re-extracts instead of handing back the * previous answer. */ discoveryInputs?: (files: readonly string[]) => string[]; /** Wire protocol for the produced boundary bindings, e.g. "http". */ protocol: string; discovery: PythonDiscoveryPattern[]; /** * Modules the project itself supplies, the wrappers a person names * when configuring the pack. The library's own module is not one of * these: it lives outside the project and would never resolve. The * adapter checks each of these against the project's roots, because * a wrapper that resolves to nothing does not match any decorator * and never says why (#188). */ projectModules?: string[]; /** The callables the library gives a project for making a request. */ clients?: PyClientCall[]; /** What the library's own database queries look like. The README says how one is matched. */ storage?: StoragePattern[]; /** Which of the library's calls give back one of a model class. The README says what a chain of them composes into. */ models?: PyModelQueries[]; /** Which of the library's classes return themselves from `__enter__`, so a `with` block gets the object itself. */ contextManagers?: PyContextManager[]; /** How the library lets a project hand the database SQL it wrote itself. */ rawSql?: RawSqlPattern[]; } /** * A call talks to the database when the method behind it says it returns one * of the library's query types. Matching on the return rather than on the * import is what reads a project base class that wraps the library, which is * how the measured corpus writes every one of its queries. */ interface StoragePattern { /** The module a query type is imported from, `sqlalchemy.orm` for a Session. */ module: string; /** Type names that mean the value is a query against the database. */ queryTypes: string[]; /** Chain-ending methods that change what is stored. Anything else reads. */ writes: string[]; /** * Methods on a query type that touch no rows of their own: one that runs * a statement built elsewhere, `session.execute(stmt)`, whose own chain * already records the work, and one that manages the session, `close`. * A call to one of these records nothing. */ recordsNothing?: string[]; /** * Methods whose keywords supply column values rather than pick rows, * `values` in `update(User).where(id=1).values(name="x")`. Their * keywords are reported as fields; every other call's keywords are the * selector. */ valueMethods?: string[]; /** * Functions the library exports that start a query on their own, for the * case where a call site imports one rather than reaching it through a * project class. `select(...)` in SQLAlchemy 2.0 is one. */ queryFunctions?: string[]; /** Which database the library is talking to, for the boundary binding. */ storageSystem: "postgresql" | "mysql" | "sqlite"; } /** * What a library gives back when a call is passed one of a project's * model classes, so a method read off the result runs the one that * class declares. SQLAlchemy and SQLModel take the class as an argument, * `session.get(User, id)` and `select(User)`, rather than as the * receiver a Rails finder is called on. */ interface PyModelQueries { /** * The names a model's ancestry arrives at: a base class the library * exports, `DeclarativeBase`, or the function that builds one, * `declarative_base`. */ baseNames: string[]; /** Methods whose result is the model again: one row, or a query a later read narrows to one. */ givesBack: string[]; /** Methods that take the model class and give back one of it, with the position it is written at. */ entryMethods: PyModelEntryMethod[]; /** Functions the library exports that do the same, called on their own rather than read off a session. */ entryFunctions: PyModelEntryFunction[]; /** The library's own constructors for a field that reaches another model. */ relationships?: PyRelationshipConstructor[]; } /** * `items: list[Item] = Relationship(...)`: the callable a model's field * is given to say it reaches another model, and where it comes from. A * project function spelled the same way is somebody else's, so the * import is what settles a match. */ interface PyRelationshipConstructor { /** The module it is imported from, `sqlmodel` or `sqlalchemy.orm`. */ module: string; /** Its name, `Relationship` or `relationship`. */ name: string; } /** `session.get(User, id)`: the method, and where the class is written. */ interface PyModelEntryMethod { method: string; argument: number; } /** `select(User)`: the function, the module it comes from, and where the class is written. */ interface PyModelEntryFunction { module: string; name: string; argument: number; } /** * The function a library gives a project for handing the database a * statement written as SQL, and where it comes from. SQLAlchemy exports * `text` from `sqlalchemy`. A local function of the same name is * somebody else's, so the import is what settles a match. */ interface RawSqlPattern { /** The module the function is imported from. */ module: string; /** The functions that take a statement written as SQL. */ functions: string[]; /** Which database the library is talking to, for the boundary binding. */ storageSystem: "postgresql" | "mysql" | "sqlite"; } type PythonDiscoveryPattern = DecoratedClassRoute | DecoratedFunctionRoute; /** * A class whose `__enter__` gives back the object it was called on, so * `with X() as y` makes y the `X()` the block opened. Python * lets `__enter__` return anything, so only the library that wrote the * class can say this, and a pack declares it for its own classes alone. */ interface PyContextManager { /** The module the classes come from, `httpx`. */ module: string; /** The class names the library documents as returning self. */ returnsSelf: string[]; } /** * The callables a library gives a project for making a request. A * function that calls one of them is a client of the boundary that call * states, and gets a unit bound to its method and path. */ interface PyClientCall { type: "clientCall"; /** The module the callables come from, `requests`. */ importModule: string[]; /** Attribute names that state the method themselves: `get` means GET. */ verbAttributeNames: Record; /** Where a call whose name states the method states its URL. */ url: { position: number; keyword: string; }; /** A call that takes the method as an argument instead, `request("GET", url)`. */ methodCall?: { attribute: string; methodPosition: number; methodKeyword?: string; urlPosition: number; }; /** Constructors whose instances take the same calls, `Session`. */ receiverConstructors?: string[]; /** What the response object gives a caller, so a guard on one of its members counts as a guard on a status. */ response?: PyClientResponse; } /** * The members a library's response object gives a caller. A caller that * tests one of them is saying which statuses it handles, and the * checker compares that against what the other side produces. */ interface PyClientResponse { /** Members whose value is the status code: `status_code`. */ statusCode?: string[]; /** Members that say the request succeeded, meaning a status in 200 to 299: `ok`. */ success?: string[]; /** Members that give the body: `json`, `text`, `content`. */ body?: string[]; /** Whether a refused request comes back as a response or raises where it was made. */ failureDelivery?: "response" | "exception"; } /** Conventions both kinds of route share. Each one describes what the library does, never a project's choice. */ interface RouteConventions { /** How the library spells a path parameter. The README lists the syntaxes we know how to read. */ pathParamSyntax?: string; /** Set it only when the library itself binds an annotated local class to the request body. */ annotatedClassIsRequestBody?: boolean; /** * Callables the library uses to inject a parameter rather than read it * off the request. FastAPI's `Depends` and `Security` are these: the * server supplies the value and the client sends nothing, so a * parameter defaulted to one of them is no part of the request however * its annotation reads. */ injectedParameterCallees?: string[]; /** * What the library serves when a composed path ends up with repeated * slashes in it. Werkzeug serves the merged path and redirects * the written one, so "merged" is what Flask needs; "kept" is the * default and is what Starlette does. */ pathRepeatedSlashes?: PathRepeatedSlashes; /** The status the library returns for a declared response when the route does not give one. Library-defined. */ defaultStatusCode?: number; /** * Set it only when the library reads a status out of the tuple a handler * returns, as Flask does with `return body, 201`. Without it * the library default applies whatever the body returns, and a route * that sets its own status would be reported at the default. */ statusFromReturnedTuple?: boolean; /** * The library's own callables that end the request with a status, such * as FastAPI's `HTTPException` and Flask's `abort`. Declaring them is * also what makes a `raise` in a route body an outcome of its own: a * raise the list does not cover comes out as a throw with no status, * and one it does cover comes out as the response the library sends. */ responseStatusCalls?: PyStatusCall[]; /** * The library's own classes whose instance, when a body returns it, is * the response the library sends, and where each takes the status: * Starlette's `JSONResponse(status_code=...)`. A return of one says its * own status; a return of anything else keeps the declared one. */ responseConstructors?: PyStatusCall[]; /** Unset means the library has no router mounting, and a route's decorator path is used as written. */ routerComposition?: RouterComposition; /** The ways the library runs a project's own function around a route. The README lists what each one covers. */ wrappers?: PyWrapperForm[]; } type PyWrapperForm = PyDependencyForm | PyDecoratedWrapperForm; /** Where a wrapper is registered, and which routes the registration reaches. */ interface PyWrapperRegistrar { /** The constructor of the object the registration is written on, as the library exports it: `FastAPI`, `APIRouter`. */ constructorName: string; /** Where the constructor is imported from, when that is not the pattern's own `importModule`: `flask` for the app a flask-restx API is served by. */ importModule?: string[]; /** * `everyRoute` for the app, whose registration reaches every route of * the pack in the run. `ownRoutes` for a router or a blueprint, whose * registration reaches the routes decorated on that same object. */ covers: "everyRoute" | "ownRoutes"; } /** * The library calls a project function before the handler, given as an * argument to one of its own callables: FastAPI's `Depends(get_user)`. * One may be written as a parameter default or inside `Annotated[...]` * on the route, or in a list under `keyword` on the route decorator or * on one of the registrars. The function runs before the handler and * ends the request by raising, so it is a wrapper whose every return * hands on. */ interface PyDependencyForm { type: "dependency"; /** The callables that take the function: `Depends`, `Security`. */ callees: string[]; /** The keyword a list of them is written under: `dependencies`. */ keyword: string; registrars: PyWrapperRegistrar[]; } /** * A project function decorated with a method on the app or a router: * `@app.middleware("http")`, `@app.exception_handler(ValueError)`, * `@app.before_request`. What the decorated function's returns mean * depends on which of the three fields below is set; with none set, * every return hands the request on. */ interface PyDecoratedWrapperForm { type: "decoratedWrapper"; /** The decorator's attribute name on the registrar: `middleware`. */ attribute: string; registrars: PyWrapperRegistrar[]; /** * The position of the parameter the wrapper calls to run what it * wraps, `call_next` at 1 for Starlette middleware. A return before * that call ends the request with what is returned. */ continuationParam?: number; /** * Set when a value returned ends the request and only a bare return * hands on. Flask's `before_request` works this way. */ returnedValueResponds?: boolean; /** The position the library hands the raised exception at. Set on an error handler, which runs only when the handler raised. */ throwParam?: number; } /** * One callable that ends the request, and where it takes the status. * A call may take it either way, so a pattern may state both, and the * keyword wins where an argument is written both ways. */ interface PyStatusCall { /** The callee as the file imports it, module and name together, `fastapi.HTTPException`. */ callee: string; /** The keyword whose value gives the status, FastAPI's `status_code`. */ statusKeyword?: string; /** The index of the positional argument giving the status, 0 for `abort(404)`. */ statusArgument?: number; /** What the library sends when the call states no status of its own. */ defaultStatusCode?: number; } /** * A class has a decorator whose first string-literal argument is the route path, * and each method in its body named after an HTTP verb becomes its own unit, * with the verb taken from the method name. */ interface DecoratedClassRoute extends RouteConventions { type: "decoratedClassRoute"; /** The library's own module, plus any wrapper module the person configuring the pack lists alongside it. */ importModule: string[]; /** The decorator's name as the library exports it, "route" for flask-restx's `Namespace.route`. */ decoratorName: string; /** Method name written in the class body, mapped to the HTTP verb it dispatches. */ verbMethodNames: Record; } /** * A function has a decorator whose attribute name is the HTTP verb and whose * first string-literal argument is the route path, the `@app.get(path)` * convention. The object it hangs on may be imported, or built one hop away by * a call to something imported. */ interface DecoratedFunctionRoute extends RouteConventions { type: "decoratedFunctionRoute"; importModule: string[]; /** Decorator attribute name, mapped to the HTTP verb it means. */ verbAttributeNames: Record; /** Unset leaves the return annotation as the only source for the response shape. */ responseModelKeyword?: string; statusCodeKeyword?: string; } /** * What a library calls the pieces of router mounting, so a route's served path * can be built from the literal prefixes written along the way: the router * constructor's own, the one at the call that mounts it, and, where the pack * says so, the prefix on the object the mount is called on. What each spelling * of a prefix means, and what makes a composition abstain, is the grid in the * adapter's README. */ interface RouterComposition { /** Constructor whose call builds a mountable router, FastAPI's `APIRouter`. */ routerConstructorName: string; /** Method that mounts a router onto the app, FastAPI's `include_router`. */ includeMethodName: string; /** * What the mount method calls its router parameter, FastAPI's `router`. * A call that passes the router by keyword rather than by position is * read through this. Unset reads the first argument only. */ routerKeyword?: string; /** One keyword serves the constructor and the mount alike. A library that spells them apart needs two fields here. */ prefixKeyword: string; /** Default "prefixes". */ mountPrefixEffect?: MountPrefixEffect; /** Set it when the library works out a path for a router that gives no prefix. We cannot work that path out, so such routes abstain. */ constructorPrefixRequired?: boolean; /** Default "unreadable". The same setting covers the constructor and the mount. */ noValuePrefix?: NoValuePrefix; /** Default "kept". */ constructorPrefixTrailingSlash?: PrefixTrailingSlash; /** * Where the object the mount is called on states a prefix of its * own, in front of everything the constructor and the mount state. * Unset means it states none, which is FastAPI's behavior: an app * serves a mounted router exactly where the two prefixes put it. */ mountObjectPrefix?: MountObjectPrefix; } /** * The prefix the object a mount is called on states, and where it is * written. flask-restx needs both halves: `Api(prefix=...)` states one * on the object itself, and the Flask blueprint the `Api` was built * from states another with `Blueprint(name, __name__, * url_prefix=...)`. The library serves a route under the blueprint's * prefix, then the `Api`'s, then whatever the namespace and the route * say. */ interface MountObjectPrefix { /** Keyword stating a prefix on the mount object's own construction (flask-restx's `Api(prefix=...)`). */ prefixKeyword?: string; /** The object handed to that construction which states a prefix of its own. */ carrier?: MountObjectCarrier; } /** * An object handed to the mount object's constructor, one hop further * out, with a prefix of its own (the Flask blueprint behind an * `Api`). Naming it here is what lets the adapter tell that object * apart from the plain app that appears in the same argument position and * has no prefix at all. */ interface MountObjectCarrier { /** Modules the carrier's constructor is imported from (Flask's `flask`). */ importModule: string[]; /** Constructor building the carrier, as its library exports it (Flask's `Blueprint`). */ constructorName: string; /** Position of the carrier among the mount object's constructor arguments. */ argumentIndex: number; /** Keyword stating the carrier's prefix, at its construction and at its registration alike (Flask's `url_prefix`). */ prefixKeyword: string; /** * Method handing the carrier to an already-built mount object * (flask-restx's `init_app`, the application-factory spelling of * `Api(blueprint)`). Unset means the constructor argument is the * only way in. */ handoffMethodName?: string; /** * Method registering the carrier somewhere else (Flask's * `register_blueprint`). The adapter reads it only to abstain: a * registration restating the prefix, putting the carrier inside * another carrier, or happening twice moves the served path * somewhere the carrier's own construction no longer says. */ registerMethodName: string; } type MountPrefixEffect = "prefixes" | "replaces"; type PrefixTrailingSlash = "kept" | "trimmed"; type NoValuePrefix = "unstated" | "unreadable"; /** * Werkzeug serves the merged path and redirects the written one, so Flask * needs "merged". Starlette leaves the path as composed, so FastAPI keeps * the default. */ type PathRepeatedSlashes = "kept" | "merged"; /** * The lexical binder. It builds module, class, and function scopes over one * file's parse tree and records how each name in each scope came to be bound, * whether that was an import, an assignment, a def, a parameter, or a `global` * or `nonlocal` redirect to another scope. * * It never has to be complete, because "I could not resolve this name" is a * legal answer everywhere a name is read. What it has to avoid is being wrong. * So it only binds names written directly in a body's own statement list. A * definition or import nested inside an `if`, `try`, or `with` block is not * found, and a read of it comes back unresolved rather than as a guess. */ type ScopeKind = "module" | "class" | "function"; interface Scope { kind: ScopeKind; /** The module node, or the class_definition / function_definition this scope belongs to. */ node: Node; parent: Scope | null; bindings: Map; } /** * `relativeLevel` is 0 for an absolute import and the dot count for a * relative one. `module` is the dotted path after the dots, empty for * a bare `from . import c`. */ type Binding = { kind: "import"; module: string; relativeLevel: number; localName: string; /** * False for `import a.b.c`, where the name binds the package `a` * and a member read off it lands somewhere other than `module`. */ bindsWholeModule: boolean; } | { kind: "importFrom"; module: string; relativeLevel: number; importedName: string; } | { kind: "classDef"; node: Node; } | { kind: "functionDef"; node: Node; } | { kind: "parameter"; } /** The right-hand side, when there is one, so we can trace a decorator's base object one hop back to whatever constructed it. */ | { kind: "assignment"; value: Node | null; } /** `global x` inside a function: reads of `x` in this scope resolve in the module scope instead. */ | { kind: "global"; } /** `nonlocal x` inside a function: reads of `x` resolve in the nearest enclosing function scope. */ | { kind: "nonlocal"; }; interface ModuleBinding { moduleScope: Scope; /** The scope a class_definition, a function_definition, or the module node opens, keyed by that node's id. */ scopeFor: Map; /** The modules a `from X import *` pulls in. We cannot list what a wildcard brings in, so nothing expands them here. */ openImports: string[]; } declare function bindModule(root: Node): ModuleBinding; /** * Python does not put a class body's namespace on the lookup chain of a function * nested inside it, so a class scope's own bindings only count when the search * starts there. A `global` or `nonlocal` marker sends the lookup to the scope it * points at. */ declare function resolveName(scope: Scope, name: string): Binding | null; /** One file, already parsed and bound. `buildRouterIndex` takes a project as a list of these. */ interface BoundPythonFile { /** The absolute path, which is what module resolution joins on. */ file: string; /** The path a gap refers to this file by, which a reader has to be able to open. */ displayPath: string; root: Node; module: ModuleBinding; } /** * What the object a decorator hangs on turns out to be. `notRouter` covers the * app itself and anything the index never saw constructed, so the decorator's * own path stands as written. `composed` gives the prefix to put in front of * that path. An `abstain` reason is written to follow "the router this route is * declared on ...". */ type RoutePrefixResolution = { kind: "notRouter"; } | { kind: "composed"; value: string; } | { /** * The router is mounted more than once, at prefixes that do not * agree. Both are served at run time, so discovery emits one * boundary per prefix rather than none (#689). */ kind: "composedMany"; values: string[]; } | { kind: "abstain"; reason: string; }; interface RouterIndex { resolve(pattern: PythonDiscoveryPattern, module: ModuleBinding, objectName: string): RoutePrefixResolution; /** * The same for a decorator whose object has no variable name to look up, * such as one written on `self.router`. The rules give the call that * built it, and every construction is already keyed by its call. */ resolveConstruction(pattern: PythonDiscoveryPattern, module: ModuleBinding, constructorName: string, constructionKey: string): RoutePrefixResolution; } interface RouterIndexOptions extends ModuleResolverOptions { /** The project's facts, so a loop over a call can be settled by the rules. */ facts?: Database; } declare function buildRouterIndex(files: BoundPythonFile[], packs: PythonPack[], resolverOptions: RouterIndexOptions): RouterIndex; /** * Where an imported name is defined, when the project defines it. * * The binder resolves a name inside one file only, so `item_in: ItemCreate` * with `ItemCreate` imported from a models module comes back as an import * binding and nothing more. This asks the resolution rules where the name * came from, through any re-exports on the way, and returns the class or * the assigned value the defining file binds it to, along with that file's * scopes so the definition can be read in the scope it is written in. */ interface ImportedDefinition { /** The class_definition, or the value an assignment gave the name. */ node: Node; /** The module scope of the file the definition is written in. */ moduleScope: Scope; /** The scopes of that file, keyed by the node that opens each one. */ scopeFor: Map; } /** `scope` is where the name is read, which is what keys the import in the facts. */ type ImportedDefinitionLookup = (scope: Scope, name: string) => ImportedDefinition | null; /** * Turns a Python annotation into an IR type shape. * * Nothing here infers anything. A value nobody annotated has no shape at all, * and an annotation this module does not recognize comes back as a `ref` by * name, which says only what the source called it. */ /** * `definitions` stores each converted class shape once, however many annotations * mention it. `scopeMaps` is here so a name written inside a referenced class's * body resolves too, not only one written at the annotation's use site; a class * read from another file brings that file's scopes along. `importedDefinition` * is absent when a caller reads one file on its own, and an imported name is * then a ref by name and nothing more. */ interface AnnotationContext { scopeMaps: Map[]; definitions: Map; importedDefinition: ImportedDefinitionLookup | null; /** The alias values being expanded, so `A = B` and `B = A` end rather than recurse. */ expanding: Set; } /** `scope` is where the annotation is written, which is how we tell a project-local class from an external name. */ declare function annotationToShape(typeNode: Node, scope: Scope, ctx: AnnotationContext): TypeShape; /** Exported so a decorator keyword that gives a class name reads the same way as that name written in annotation position. */ declare function shapeFromName(name: string, scope: Scope, ctx: AnnotationContext): TypeShape; interface Range { start: number; end: number; } /** An argument as written, plus its node so a reader can evaluate what it comes down to. */ type DecoratorArg = DecoratorArgShape & { readonly node: Node; }; type DecoratorArgShape = { kind: "string"; value: string; } | { kind: "number"; value: number; } | { kind: "boolean"; value: boolean; } | { kind: "none"; } /** A bare name. The caller resolves it, using the scope it already has. */ | { kind: "identifier"; name: string; } /** One dotted hop, `routers.orders`. The caller decides what the object is. */ | { kind: "attribute"; objectName: string; attributeName: string; } | { kind: "list"; items: DecoratorArg[]; } | { kind: "other"; }; interface DecoratorClassification { /** The name as its source module exports it, not the local alias or attribute path it was written under. */ importedName: string | null; /** The dotted module the name was imported from. It is null whenever `importedName` is null. */ module: string | null; /** The local variable an attribute decorator hangs on, `app` in `@app.get(...)`. */ objectName: string | null; args: DecoratorArg[]; keywordArgs: Record; /** Where the decorator is written. Anything we read out of its arguments uses this as its provenance. */ range: Range; /** * The module the decorator's object lives in, when the decorator was read * through a project wrapper written in another file. The router prefix is * resolved there, since that is where the namespace is constructed. */ objectModule?: ModuleBinding; /** * The call the rules say built the object this decorator hangs on. A * router the index never saw under a name is looked up by this call * instead, and a decorator on `self.router` has no name to look up. */ subjectConstruction?: { key: string; constructorName: string; }; } declare function classifyDecorator(decoratorNode: Node, module: ModuleBinding, facts?: Database): DecoratorClassification; /** * What discovery uses for one file. `factsPath` is the path the facts were * keyed under, which is the absolute one, while a summary shows the short one. */ interface StorageLookup { readonly facts: Database; readonly factsPath: string; readonly patterns: readonly StoragePattern[]; readonly definitionAt: (key: string) => Node | undefined; readonly couldMatch: ReadonlySet; /** What a pack says about statements a project writes as SQL itself. */ readonly rawSql?: readonly RawSqlPattern[]; } /** * What a call's callee is: a function in this run the walk can step * into, or a reason it cannot. * * The rules in @suss/resolution decide it. Every language feature that * moves a value is a hop they already state, so nothing here reads a * name, an alias, an attribute, or an instance for itself. * * What is left is about files rather than values: a module before the * dot, and a name only a wildcard import could have brought in. */ /** A function in this run, and the export path its summary gets. */ interface ReachedFunction { readonly file: BoundPythonFile; /** The `function_definition` node. */ readonly node: Node; readonly name: string; /** `[name]` for a module function, `[Class, name]` for a method. */ readonly exportPath: string[]; } /** * The project functions a library runs around a route, read from where * they are registered: a dependency on a route's parameter or decorator, * a dependency list on the app or a router, a function decorated with * `@app.middleware(...)` or `@app.exception_handler(...)`. Each one * becomes a unit of its own, and every route it reaches lists it, so the * extractor can fold its outcomes into the route's. The README says which * registrations are read and which are not. * * A registration on the app reaches every route of the pack in the run, * since an app is one per run for every pack that has one. A registration * on a router reaches the routes decorated on that same router object, * and nothing mounted onto it. */ interface WrapperIndexOptions { packs: readonly PythonPack[]; roots: string[]; facts: Database | undefined; /** The function each function key was read from. */ definitions: ReadonlyMap; storageFor: (file: BoundPythonFile) => StorageLookup | undefined; } /** The route asking which wrappers reach it: the decorator it was found by, and where that decorator was read. */ interface RouteWrapperQuery { pack: PythonPack; pattern: PythonDiscoveryPattern; /** The absolute path of the file the route is written in. */ file: string; module: ModuleBinding; classification: DecoratorClassification; definitionNode: Node; } /** A wrapper the index has a unit for, and the form it was registered through. */ interface Registered { reference: WrapperReference; form: PyWrapperForm; } interface FormOf { pack: PythonPack; pattern: PythonDiscoveryPattern; form: PyWrapperForm; } /** What every registration in the run reaches, asked once per route and then once per file for the wrapper units. */ declare class PythonWrapperIndex { readonly options: WrapperIndexOptions; readonly filesByPath: ReadonlyMap; /** By pack name: what the app registered, in the order the forms are declared and then the order the files were read. */ private readonly everyRoute; /** By construction key: what a router or a blueprint registered. */ private readonly ownRoutes; private readonly unitsByKey; private readonly unitsByFile; constructor(options: WrapperIndexOptions, filesByPath: ReadonlyMap); /** The project function a module-level name in this file refers to, or null for anything else. */ functionCalled(file: string, name: string): ReachedFunction | null; register(covers: { kind: "everyRoute"; pack: string; } | { kind: "ownRoutes"; key: string; }, target: ReachedFunction, declared: FormOf): void; /** The unit for a function, built once however many registrations point at it. */ registered(target: ReachedFunction, declared: FormOf): Registered; wrappersFor(query: RouteWrapperQuery): WrapperReference[]; private ownRoutesOf; /** * Where the decorator's object was built: the route's own file, the file * a project wrapper function is written in, or the file an imported * router comes from. The name is the one that file binds it under. */ private objectSiteOf; private fileOfModule; unitsIn(file: string): RawCodeStructure[]; } /** * Finds decorated routes and turns each one into a `RawCodeStructure`. * * Only decorated module-level functions and classes that are declared * unconditionally get discovered, the same boundary the binder in scope.ts * draws. Nothing here reads into a unit's body, so a route's summary comes out * with no branches at all, or with exactly one branch describing what an * annotation or a decorator keyword already states, such as a FastAPI * `response_model` or `status_code`, or a return annotation. * * That is enough to pair a route against a caller by method and path, and it * claims nothing about behavior nobody read. */ interface DiscoveryOptions { packs: PythonPack[]; /** Repo-relative or absolute path recorded on each summary's `location.file`. */ filePath: string; /** Without it, every route object looks like the app itself and paths stand as written. */ routerIndex?: RouterIndex; /** Under "strict" a route whose unit cannot be built stops the run instead of abstaining. */ gapHandling?: ExtractorOptions["gapHandling"]; /** What a pack needs to say a call talks to the database. Absent when no pack does. */ storage?: StorageLookup | undefined; /** The file's absolute path, which module resolution wants; `filePath` may be shortened for display. */ absoluteFile?: string | undefined; /** The project's facts, so the rules can say what an object no scope has a binding for was built by. */ facts?: Database | undefined; /** What the project registered around its routes. Absent when no pack declares a wrapper form, or when a caller reads one file on its own. */ wrappers?: PythonWrapperIndex | undefined; /** Where a name another file defines is written, so an imported model or alias reads as the class or value behind it. Absent when a caller reads one file on its own. */ importedDefinition?: ImportedDefinitionLookup | undefined; } declare function discoverUnits(root: Node, module: ModuleBinding, options: DiscoveryOptions): RawCodeStructure[]; /** Ask what these calls come down to, then derive. */ declare function resolveCalls(db: Database, callKeys: readonly string[]): void; /** * The values an object contains under its own keys, in the order the source * writes them. Empty when nothing said what the object contains. */ declare function containedValues(db: Database, objectKey: string): string[]; /** What a call comes down to, when the rules settled it on an object. */ declare function objectReturnedBy(db: Database, callKey: string): string | null; /** * values.ts: the facts @suss/resolution already joins, emitted for Python. * The relation names and shapes come from that package's own header, so a * Python value follows the same rules a TypeScript one does. * * A name is keyed by the scope that binds it. A function's own names take * the function's key, so two handlers that both write `query` stay apart, * and a module's names take the file's key, which is what another file * imports back out. Every write to a name in one scope is collected in * source order and `valueLeftByWrites` says what the name comes down to, * so a reassigned name states one value or none rather than two. */ /** * A node's identity across the whole run. The end is part of it because a * call and its callee start at the same offset. */ declare function nodeId(filePath: string, node: Node): string; /** * The key a read of this expression joins on, for a caller that has an * expression in hand and wants to ask the rules about it. `enclosing` is * the function the expression is written in, or null at module level. */ declare function readKey(filePath: string, node: Node, enclosing: Node | null): string; /** * The key a name joins on, for a caller holding the name as text and the * function it is written in. */ declare function nameKeyIn(filePath: string, enclosing: Node | null, name: string): string; /** * Walk a module and emit the value facts. A nested function is walked in its * own right, so a body's returns and calls belong to the function that wrote * them. */ declare function emitValueFacts(db: Database, filePath: string, root: Node): void; /** * The session behind `suss ask why` on a Python project: a tree-sitter * parse of the asked-about source, a way to point at the expression * somebody spelled, and the witness proof of what it resolved to, * rendered through `@suss/resolution`'s phrases. * * It parses every file under the root and emits the same value facts * `extractPythonProject` does, keeping a location for every fact key so * a proof's atoms can point back at source. A handle this session hands * back pairs a tree-sitter node with the file it came from, since a * node alone does not say which file parsed it. */ interface PythonWhySessionOptions { /** The project root, which paths in every answer come out relative to. */ dir: string; } /** A found node, paired with the file it was parsed from. */ interface PythonValueHandle { file: string; node: Node; } declare class PythonWhySession { private readonly root; private readonly db; private readonly locations; private readonly trees; constructor(options: PythonWhySessionOptions); /** * The smallest expression on `line` of `file` whose text is exactly * `text`, or null. */ findExpression(file: string, line: number, text: string): PythonValueHandle | null; /** * The callee of the call written as `calleeText` between `startLine` * and `endLine` of `file`, or null. */ findCallee(file: string, startLine: number, endLine: number, calleeText: string): PythonValueHandle | null; /** * Why `value` resolves to what it does: the witness proof, flattened * to the chain and rendered. Null when the value does not resolve, * which the caller says in its own words. */ explain(value: PythonValueHandle, options?: { maxDepth?: number; }): WhyExplained | null; private locate; private rootOf; private pathOf; /** A file path said relative to the root, or as given when it is outside the root. */ private displayPath; } /** * The name is part of the key because the range is measured in lines, two * units can start on the same line, and `entry` is a set, so keying on the range * alone would drop one of them. */ declare function unitKey(filePath: string, range: { start: number; end: number; }, name: string): string; declare function emitEntryFact(db: Database, filePath: string, range: { start: number; end: number; }, name: string): void; /** * Every import in the file, wherever it is written. Python code puts an import * inside a function to break a cycle between two modules, and a chain through * one of those functions stops dead without it. */ declare function emitModuleImportFacts(db: Database, filePath: string, module: ModuleBinding, resolverOptions: ModuleResolverOptions): void; interface ExtractPythonOptions { /** Absolute paths of the files to parse and extract. */ files: string[]; packs: PythonPack[]; /** Directories an absolute import is resolved against. */ roots: string[]; /** When set, `location.file` on each summary is relativized against this. */ workspaceRoot?: string; /** The directory a summary's id measures its file from, when that differs from `workspaceRoot`. */ projectRoot?: string; /** As well as deciding how much of what nobody could read reaches a summary, "strict" lets a route that cannot be built stop the run. */ gapHandling?: ExtractorOptions["gapHandling"]; /** Called once with the run's per-phase wall time, for `suss extract --timing`. */ onTiming?: (report: TimingReport) => void; /** Called once with the file-by-file funnel, for `suss extract --explain`. */ onExtractionReport?: (report: ExtractionReport) => void; /** Called once with what the cache decided, for `suss extract --timing`. */ onCacheDiagnostic?: (diagnostic: CacheDiagnostic) => void; /** Absolute. `/.suss/cache` by default; `null` turns it off. */ cacheDir?: string | null; } interface ExtractPythonResult { summaries: BehavioralSummary[]; facts: Database; } /** One parsed file and the packs a run over it would load. */ interface FileFactsOptions { file: string; root: Node; module: ModuleBinding; packs: readonly PythonPack[]; } /** * The facts for a single parsed file, with the evaluator bound to them, * for a caller that has one file and no project. A pack's own tests need * these: what built a receiver is an answer the rules give, and without * facts they have nothing to give it from. A project run emits the same * facts across every file at once, so a name written in another file * resolves there and never here. */ declare function factsForFile(options: FileFactsOptions): Database; declare function extractPythonProject(options: ExtractPythonOptions): Promise; /** Every `.py` file under `root`, depth-first, skipping the usual non-source directories. */ declare function findPythonFiles(root: string): string[]; /** * Evidence for drafting a project-wrapper dependency stub: every * import, anywhere in the project, of the asked module or one of its * submodules. A Python decorator pattern matches a wrapper's import * module exactly (see discovery.ts's `importModule.includes`), so a * stub covers one imported module at a time rather than one package, * and this groups by the exact module text a project writes. * * This walks every node rather than going through the binder in * scope.ts, so an import inside an `if` or `try` still counts as * evidence even though the binder would not resolve a name through it. */ interface PythonImportSite { /** The name imported at this site, null for a bare `import module`, which binds the whole module rather than one name from it. */ name: string | null; file: string; line: number; } interface PythonImportEvidence { /** The full dotted module text, exactly as a stub's `package` would spell it. */ module: string; sites: PythonImportSite[]; } interface PythonImportEvidenceOptions { packageName: string; directory: string; } /** * Every import of `packageName`, or a submodule of it, across the * project's own files, grouped by the exact module each was imported * from. */ declare function pythonImportEvidence(options: PythonImportEvidenceOptions): Promise; /** * version.ts: this adapter's own half of the cache key. * * `ADAPTER_VERSION` is the hand-bumped semver. Bump it on any change * that affects extraction output: IR shape, discovery semantics, * terminal classification, anything that would invalidate previously * cached summaries. */ declare const ADAPTER_VERSION = "0.1.0"; export { ADAPTER_VERSION, type AnnotationContext, type Binding, type BoundPythonFile, type DecoratedClassRoute, type DecoratedFunctionRoute, type DecoratorArg, type DecoratorClassification, type DiscoveryOptions, type ExtractPythonOptions, type ExtractPythonResult, type FileFactsOptions, type ModuleBinding, type ModuleResolution, type ModuleResolverOptions, type MountObjectCarrier, type MountObjectPrefix, type MountPrefixEffect, type NoValuePrefix, type PathRepeatedSlashes, type PrefixTrailingSlash, type PyModelEntryFunction, type PyModelEntryMethod, type PyModelQueries, type PyStatusCall, type PythonDiscoveryPattern, type PythonImportEvidence, type PythonImportEvidenceOptions, type PythonImportSite, type PythonPack, type PythonValueHandle, PythonWhySession, type PythonWhySessionOptions, type RawSqlPattern, type RelativeModuleSpec, type RouteConventions, type RoutePrefixResolution, type RouterComposition, type RouterIndex, type Scope, type ScopeKind, type StoragePattern, annotationToShape, bindModule, buildRouterIndex, classifyDecorator, containedValues, discoverUnits, emitEntryFact, emitModuleImportFacts, emitValueFacts, extractPythonProject, factsForFile, findPythonFiles, nameKeyIn, nodeId, objectReturnedBy, parsePython, parsePythonSync, preloadPythonGrammar, pythonImportEvidence, readKey, resolveAbsoluteModule, resolveCalls, resolveModule, resolveName, resolveRelativeModule, shapeFromName, unitKey };