Build: Lint core/src and integrations/src with type information - #625
Open
AmaadMartin wants to merge 5 commits into
Open
Build: Lint core/src and integrations/src with type information#625AmaadMartin wants to merge 5 commits into
AmaadMartin wants to merge 5 commits into
Conversation
added 5 commits
August 4, 2026 01:38
… await Two defects surfaced by running ESLint with type information over core/src. AsyncQueue.error() accepted `unknown` and rejected its iterator with it. The only caller forwards the live API's ErrorEvent, which is not an Error, so consumers lost the stack trace and `instanceof Error` was false. The parameter is now `Error` and the caller wraps the event, keeping it as `cause`. getCanonicalUri() awaited getArtifactDir(), which returns a plain string; the other six call sites do not await it.
Adds a files-scoped type-aware ESLint block wired to the existing per-package tsconfigs, so the type-checked rules stop being inert. Eleven rules that reach zero findings are enabled, including no-floating-promises and no-misused-promises; the rest are off with their measured counts. The block sits ahead of the "**/*.ts" block because recommendedTypeCheckedOnly bundles typescript-eslint's eslint-recommended, which switches off 18 core rules (no-undef, no-const-assign, no-unreachable, ...) that js/recommended re-enables afterwards. An integration test pins that ordering along with the scope boundary. The no-undef directive in rest_api_tool.ts is dropped: type-aware parsing resolves BodyInit as a type-only reference, so the directive is now unused.
lintText() with overridden content is not a reliable way to exercise a type-aware rule: the TypeScript program reads source from disk, so whether the override is type-checked depends on the environment. It reported the floating promise locally and returned no messages on all three CI runners. The probe is now a real file written under core/src (and core/test for the negative case) and removed in a finally block, which exercises the same path a contributor hits.
…nment The previous assertions both depended on how the TypeScript program resolves source that is not on disk at parse time. Overriding content with lintText() reported the floating promise locally but returned nothing on all three CI runners; writing a probe file into core/src produced a fatal parse error there. Enabling a rule that requires a program over already-existing source has no such dependency: it runs clean for core/src and throws 'requires type information' for the test tree, which is the distinction being pinned.
Inline TYPE_CHECKED_SOURCES; it had one reader thirteen lines below its declaration. Reword the deferred-rules header so the list reads as a burn-down of pre-existing findings rather than a standing exemption. No TODO id is written because the repository has no public tracking issue for the work; four follow-up tasks covering all twelve rules are queued instead. Type severityOf with Linter.RuleEntry / Linter.RuleSeverity. calculateConfigForFile is declared Promise<any>, so narrowing an unknown with Array.isArray laundered the element type back through any and needed a cast to return a number. There is now no type assertion anywhere in the diff.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
Link to an existing issue (if applicable):
N/A
Or, if no issue exists, describe the change:
Problem: ESLint runs today with no type information. The root
eslint.config.jsnever setslanguageOptions.parserOptions.project, so every rule that needs the type checker is inert —no-floating-promises,no-misused-promises,await-thenable, theno-unsafe-*family. In a codebase built on async generators, live bidirectional model connections and MCP sessions, that is a large unlinted class of defect.Solution: One
files-scoped type-aware block ineslint.config.jscoveringcore/src/**/*.tsandintegrations/src/**/*.ts, wired to the existingcore/tsconfig.jsonandintegrations/tsconfig.json. No new tsconfig, no tsconfig edits, no new dependency, no script change. Every type-checked rule was measured; the 11 that reach zero findings are enabled, and the 12 that do not are explicitlyoffwith their measured counts recorded in the config and below.Two real defects that the type checker surfaced are fixed at the source (see Behaviour changes).
Collision check (done before any code was written)
gh pr list --repo AmaadMartin/adk-js --state open --limit 1000(524 open PRs) plusgh pr diff --name-onlyon every plausibly adjacent PR.tsconfig.eslint.jsoncoveringdev/**andtests/**too, plus the 82-siteno-unnecessary-type-assertionsweep. This PR deliberately does not create a tsconfig and scopes to the two published source trees. These are two mutually exclusive answers to the same question and only one should land — flagging rather than silently competing. If Build: Give ESLint type information and enable no-unnecessary-type-assertion (Part 1/2) #332 is preferred, close this one; the triage table and the block-ordering finding below still apply there.eslint.config.js(Fix: emit the node: protocol in the ESM build banner and format-check .js/.mjs (stacked on #426) #552, Chore: enforce the node: protocol for Node built-in imports in src (no new deps) #548, Chore: report unused eslint-disable directives as errors #496, Chore(lint): forbid deep imports into the @google-cloud/vertexai build output #475, Chore: remove the dead dev/src/browser ignore entries from eslint.config.js and .prettierignore #454, Chore: fail lint on deep @google/adk* subpath imports (stacked on #380) #435, Chore: lint JavaScript files, not just TypeScript #426, Chore: enforce the node: protocol for Node built-in imports (ESLint) and sweep 56 violations #422, Chore: enforce the node: protocol for Node built-in imports (ESLint) #346, Fix: ban deep subpath imports of ADK workspace packages (ESLint guard) #325, Feat: fail the build on phantom dependencies in the published src trees (import/no-extraneous-dependencies) #323); none addparserOptions,projectService, or any type-aware rule, so there is no overlap with those.One correction to the approved design
The design called for appending the block at the end of the config. Measured, that silently disables 18 core correctness rules on
core/src—no-undef,no-const-assign,no-unreachable,no-dupe-keys,constructor-super, and 13 more — becausetseslint.configs.recommendedTypeCheckedOnlybundles typescript-eslint'seslint-recommended, which switches those off, and a trailing block wins over thejs/recommendedthat re-enables them. The block is therefore placed among the presets, ahead of the repo's own"**/*.ts"block, which keeps the last word. This is the layering the file already relies on. The invariant is pinned by a test, not left to comment discipline.Cost measurement
Local workstation (AMD EPYC 7B13, 64 vCPU, 117 GB RAM), Node v22.22.2,
eslint@9.39.4,typescript-eslint@8.61.1,typescript@5.9.3. Warmnpm install,npm run builddone first, ESLint cache off. Median of 3.npx eslint "**/*.ts"main)Interactive / pre-commit path (
lint-stagedrunseslint --fixon staged files):eslint --fix core/src/runner/runner.ts(type-aware)eslint --fix core/test/utils/task_test.ts(not type-aware)16.22 s is far below the ~2 min threshold at which the design asked for a
parserOptions.projectServicecomparison, so the explicitprojectarray ships as specified.projectService: truewas benchmarked anyway, since it is the documented alternative and removes the hand-maintained array: it is functionally equivalent over this scope (a rule requiring a program reports the same 82 findings, zero fatal messages) but slower — median 16.88 s (16.87 / 16.88 / 17.39) against 16.22 s, at the same peak RSS. Both numbers are reported here rather than swapping silently;projectstays.TIMING=15top 10:@typescript-eslint/no-misused-promises@typescript-eslint/no-unused-vars@typescript-eslint/await-thenable@typescript-eslint/no-floating-promises@typescript-eslint/unbound-methodno-redeclareno-unexpected-multilineconstructor-superno-regex-spaces@typescript-eslint/ban-ts-commentconstructor-super,no-redeclare,no-unreachable,no-this-before-superandno-dupe-keysappearing here is independent confirmation that the 18 core rules are still executing on the type-checked files.Triage: every type-checked rule, measured
Counts are over
core/src+integrations/src; no rule fired anywhere else.Enabled — 11 rules at zero findings.
no-floating-promisesno-misused-promisesunbound-methodonly-throw-errorno-implied-evalno-for-in-arrayno-array-deleteno-duplicate-type-constituentsno-unsafe-unary-minusawait-thenableprefer-promise-reject-errorsDeferred — 12 rules, 285 findings, each
offwith its count in the config.no-unnecessary-type-assertioneslint --fixsweep; its own review.require-awaitasyncinterface overrides with nothing to await; enabling it tends to produce exactly theeslint-disablecomments this change avoids.no-unsafe-member-accessJSON.parseboundaries.no-unsafe-assignmentJSON.parseboundaries.restrict-template-expressionsunknown-> message helper for the 83catchclauses.no-unsafe-enum-comparisonTaskState/MessageRoleenums while the A2A SDK types those fields as string-literal unions. Reconciling the two is a design decision, not a local fix.no-unsafe-callJSON.parseboundaries.no-redundant-type-constituentsrootAgent: BaseAgent | anyon the publicApptype, entangled with a pre-existingno-explicit-anysuppression.restrict-plus-operandsno-base-to-stringllm_request.ts:69concatenatessystemInstruction, a genuine latent[object Object].no-unsafe-argumentJSON.parseboundaries.no-unsafe-returnJSON.parseboundaries.require-awaitmeasured 68 before theawait-thenablefix and 69 after: removing the strayawaitleavesgetCanonicalUriwith nothing to await. The config records the post-fix number.no-unnecessary-type-assertionis deferred rather than swept in this PR because 82 findings across 38 files is not the "few findings, each with a local fix" the enable criterion asks for, and because fork PR #332 already contains exactly that sweep — a follow-up should adopt those fixes rather than redo them. This is a stated scope decision, not an omission.Every one of the 12 is a pre-existing finding deferred for staged adoption, not a suppression this change introduces — all 285 exist on
maintoday and are simply invisible because no rule can see them. None is silenced at a call site: each is oneoffentry carrying its measured count, and the config comment says to burn the count down and re-enable. Four follow-up tasks are queued covering all 12 rules (theno-unsafe-*family via typedJSON.parseboundaries;restrict-template-expressions/no-base-to-string/restrict-plus-operandsvia a sharederror_utils.tshelper; theno-unnecessary-type-assertionsweep; andrequire-await/no-unsafe-enum-comparison/no-redundant-type-constituents). NoTODO(<id>)marker is written into the config because this repository has no public tracking issue for the work, and inventing one would be worse than the prose.Behaviour changes
Two, both from fixing a rule finding at its root rather than suppressing it:
AsyncQueue.error()now takesError, notunknown(core/src/utils/async_queue.ts). Its iterator rejected with whatever it was handed; the sole caller forwards the live API'sErrorEvent, which is not anError, so consumers lost the stack trace andinstanceof Errorwas false.core/src/models/google_llm.tsnow wraps it:new Error(error.message, {cause: error}), keeping the original event reachable. Covered by a new test.getCanonicalUrino longer awaitsgetArtifactDir(core/src/artifacts/file_artifact_service.ts).getArtifactDirreturns a plainstring; the other six call sites do not await it. No observable change — the function isasynceither way — but the rule now prevents it recurring.One
// eslint-disable-next-line no-undefis removed fromrest_api_tool.ts: with type information the parser resolvesBodyInitas a type-only reference, so the directive became unused and started emitting a warning. This change adds no suppression and removes one.Notes
npm run buildrunstsc --emitDeclarationOnlyover exactly those files in CI.include, and typescript-eslint errors on a parsed file no project contains.dev/srcis also excluded — 19 of its files import@google/adk, which resolves throughcore/dist, so its findings would depend on whether the tree happens to be built. Both are pinned as not type-aware by the test, so widening later has to be a deliberate edit.Testing Plan
Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.
tests/integration/lint_config/lint_config_test.ts(new, 15 tests, ~4.5 s) pins the gate: the project wiring andtsconfigRootDirfor both type-checked trees;no-floating-promises/no-misused-promises/await-thenableaterror; the deferred rules atoff; all 18 core rules still aterror; andcore/test/**+dev/src/**still parsed without type information. It also drives real ESLint end to end:lintFilesoncore/src/utils/task.tsreports zero errors, and — the proof that type information is genuinely loaded — enabling a rule that requires a program runs clean overcore/srcbut throwsrequires type informationfor the test tree.core/test/models/google_llm_test.ts— one new test (existing tests untouched) asserting the liveonerrorpath surfaces anErrorwith theErrorEventascause.google_llm_test.ts(22), andasync_queue+gemini_llm_connection+core/test/artifacts+core/test/tools/openapi_tool(15 files, 232 tests).npm run build,npm run lint,npm run format:check,npm run docs:checkall exit 0.Proof each new test can fail. Every mutation was applied to the real tree, run, and reverted:
eslint.config.jsexpected undefined to deeply equal [ './core/tsconfig.json', …(1) ],expected undefined to be 2, andError while loading rule '@typescript-eslint/no-unnecessary-type-assertion': You have used a rule which requires type information..."core/test/**/*.ts"toTYPE_CHECKED_SOURCESexpected [ './core/tsconfig.json', …(1) ] to be undefinedandexpected 2 to be undefined"**/*.ts"blockconstructor-super: expected +0 to be 2(this is the 18-rule regression the ordering prevents)new Error(error.message, {cause: error})wrapexpected an Error, got objectawait getArtifactDir(...)npx eslintfails:561:23 error Unexpected 'await' of a non-Promise (non-"Thenable") value @typescript-eslint/await-thenableGetting that last assertion right took three attempts, which is worth recording.
lintFilesalone passes with and without the block ("zero errors" is trivially true when no type-aware rule runs). Overriding file content vialintTextreported the floating promise locally but returned no messages on all three CI runners, and writing a probe file intocore/srcproduced a fatal parse error there — in both cases because the TypeScript program is built from disk at parse time, so the result depends on the environment. The shipped assertion touches no files and only enables an existing rule over existing source, so it behaves identically everywhere.No
eslint-disable,@ts-expect-error,@ts-ignore,any,as any,as never,as unknown as, or coverage suppression was added anywhere in this change (oneeslint-disableis removed), and the diff contains no type assertion of any kind. The test constructs a realErrorEventviaclass TestErrorEvent extends Event implements ErrorEventrather than casting, since Node has noErrorEventglobal;severityOfis typed withLinter.RuleEntry/Linter.RuleSeverityfromeslint, becausecalculateConfigForFileis declaredPromise<any>and narrowing anunknownwithArray.isArraywould launder the element type back throughany.Manual End-to-End (E2E) Tests:
The scope boundary in the middle step is also asserted automatically, so it cannot silently rot.
Checklist
[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.