| 294 | * everything else — a plain call into a library is not an effect. |
| 295 | */ |
| 296 | export function classifyEffect(input: EffectInput): Effect | null { |
| 297 | // Rules see the chain without its argument lists: `res.status(404).json` |
| 298 | // is `res.status.json`, `ResponseEntity.status(HttpStatus.NOT_FOUND).body` |
| 299 | // is `ResponseEntity.status.body`. |
| 300 | const text = normaliseCall(input.text).replace(/\([^()]*\)/g, ''); |
| 301 | if (text === '') return null; |
| 302 | const families = familiesOf(input.language); |
| 303 | const project = input.project ?? 'app'; |
| 304 | if (families.includes('swift') && SWIFT_NOT_AN_EFFECT.test(text)) return null; |
| 305 | |
| 306 | for (const rule of EFFECT_RULES) { |
| 307 | if (input.projectType && rule.category !== 'database' && rule.category !== 'response') continue; |
| 308 | if (rule.only && !rule.only.some((f) => families.includes(f))) { |
| 309 | // An ungated language (no family) still gets the JS rows — the table |
| 310 | // grew up on them and the old tests call with no language. |
| 311 | if (input.language) continue; |
| 312 | if (!rule.only.includes('js')) continue; |
| 313 | } |
| 314 | if (rule.instantiates && input.kind !== 'instantiates') continue; |
| 315 | if (rule.test.test(text)) return withShape(rule.category, text, input.args ?? null, input.language ?? null, null); |
| 316 | } |
| 317 | |
| 318 | // The receiver's declared type — a library's, never a class of the project |
| 319 | // (the caller checks): `OwnerRepository owners` makes `owners.save` the |
| 320 | // database when no row above knew the name. |
| 321 | if (input.receiverType) { |
| 322 | const type = input.receiverType.replace(/^(?:readonly|private|public|protected|final|static)\s+/g, '').trim(); |
| 323 | for (const rule of RECEIVER_TYPE_RULES) { |
| 324 | if (rule.test.test(type)) return withShape(rule.category, text, input.args ?? null, input.language ?? null, type); |
| 325 | } |
| 326 | } |
| 327 | |
| 328 | // A thrown web exception is a response, in a project that has endpoints. |
| 329 | if (project !== 'app' && (input.kind === 'instantiates' || families.includes('py') || families.includes('swift'))) { |
| 330 | // `\\InvalidArgumentException`, `Foo\\BarException`: the class, not its namespace. |
| 331 | const last = (text.split(/[.:]/).pop() ?? text).replace(/^.*\\/, ''); |
| 332 | if (EXCEPTION_RESPONSE.test(text) && !NOT_A_RESPONSE.test(last)) return { category: 'response' }; |
| 333 | } |
| 334 | return null; |
| 335 | } |
| 336 | |
| 337 | const READ_OPS = |
| 338 | /^(?:find\w*|findone|get|getone|getmany|getall\w*|getbyid\w*|getasync|list\w*|count\w*|aggregate|select\w*|query\w*|queryrow\w*|scalar\w*|first\w*|single\w*|last\w*|all|any\w*|exists\w*|tolist\w*|toarray\w*|filter|where|fetch\w*|load\w*|read\w*|scan|search|paginate|pluck|distinct|group\w*|order\w*|include\w*|join\w*|objects|values|values_list|iterator|stream|max|min|sum|avg|average|exists\?|one|one_or_none|scalars?|find_by|find_each|firstordefault\w*|singleordefault\w*|countdocuments|rows|row|preload|take|limit|offset|skip|raw|fromsql\w*|\$queryraw|get_or_404|get_object_or_404|describe\w*|head\w*|retrieve|show|index|lookup|peek|contains|has|check|isempty|is_empty|size|length|fetchone|fetchall|fetchmany|refresh)$/i; |