Merge linter settings in extended TypeSpec configs - #11924
Merge linter settings in extended TypeSpec configs#11924Sylvester Kaczmarek (sylvesterkaczmarek) wants to merge 5 commits into
Conversation
|
Azure Pipelines: Successfully started running 1 pipeline(s). 1 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
🟢 Approval recommended
The implementation matches the stated merge semantics for linter under extends, includes targeted test coverage, and adds an appropriate .chronus fix entry.
Pull request overview
This PR updates the TypeSpec compiler’s config loading behavior so that when a tspconfig.yaml extends another config, the linter block is merged one level deep (similar in spirit to how emitter options are merged), preventing unrelated inherited linter settings from being dropped.
Changes:
- Merge
parent.linterandconfig.linter(shallowly) when both are present duringextendsresolution. - Add a new config scenario and unit test covering inherited
linter.extends/linter.enablewhile overridinglinter.disable. - Add a
.chronusfix entry for@typespec/compiler.
File summaries
| File | Description |
|---|---|
| packages/compiler/src/config/config-loader.ts | Implements one-level merge for linter when resolving extends. |
| packages/compiler/test/config/config.test.ts | Adds a regression test asserting correct inherited vs overridden linter fields. |
| packages/compiler/test/config/scenarios/extends-linter/typespec-base.yaml | New base config scenario defining linter.extends, enable, and disable. |
| packages/compiler/test/config/scenarios/extends-linter/tspconfig.yaml | New child config scenario overriding only linter.disable. |
| .chronus/changes/fix-11849-merge-linter-config-2026-8-10-11-8-49.md | Changelog entry documenting the fix for @typespec/compiler. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
7729fcf to
30943e6
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
Preserve inherited ruleset source metadata and update the handbook documentation.
Review details
Suppressed comments (2)
packages/compiler/src/config/config-loader.ts:140
- When the parent linter contains a
file:ruleset reference, this merge keeps the parent rule set but discards the YAML source that declared it.program.tsalways supplies the final childconfig.fileas the linter source, so diagnostics for a missing/invalid inherited ruleset are located at offset 0 of the child config instead of the parentlinter.extendsentry. Preserve the source/origin for inherited linter settings (or carry per-entry source metadata) and add an integration test for this case.
merged.linter = { ...parent.linter, ...config.linter };
packages/compiler/src/config/config-loader.ts:140
- The handbook currently says root-level properties are shallow-merged except
options(website/src/content/docs/docs/handbook/configuration/configuration.mdx:123-128). This newlinterspecial case makes that public contract inaccurate; update the extension rules/example to document the one-levellintermerge and that each nested field is replaced wholesale.
if (parent.linter && config.linter) {
merged.linter = { ...parent.linter, ...config.linter };
- Files reviewed: 5/5 changed files
- Comments generated: 0 new
- Review effort level: Lite
Signed-off-by: Sylvester Kaczmarek <[email protected]>
|
Addressed the latest review findings in 4a7d1ab. Inherited linter fields now preserve the YAML source that supplied them, so file ruleset diagnostics point to the parent config. The handbook also documents the one-level linter merge. The config test suite passes 33/33, and the compiler TypeScript build passes. |
There was a problem hiding this comment.
🟡 Changes recommended
Two moderate issues remain in tsp info output and inherited diagnostic source locations.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
packages/compiler/src/config/types.ts:91
- This new enumerable field leaks into
tsp info:printInfoActionremovesdiagnostics,filename, andfilebefore serializing the remainingTypeSpecConfig, but does not removelinterSource. Any config with a linter will therefore print internalYamlScript/Document/source data instead of just the resolved configuration; exclude this metadata from the info output (and add a regression test).
linterSource?: Partial<Record<keyof LinterConfig, YamlScript>>;
packages/compiler/src/core/program.ts:433
- The new source selection still runs through the language-server config cache, which serializes
TypeSpecConfigwithJSON.stringify/JSON.parseinserver/compile-service.ts. That turns eachYamlScript.docinto plain JSON, sogetLocationInYamlScriptcannot traverse the YAML document and inheritedfile:diagnostics lose their reallinter.extendsrange (typically falling back to position 0); preserve/reparse the YAML document for this path and cover it through the server entrypoint.
source: options.configFile?.linterSource?.extends
? { script: options.configFile.linterSource.extends, path: ["linter"] }
- Files reviewed: 8/8 changed files
- Comments generated: 1
- Review effort level: Lite
Signed-off-by: Sylvester Kaczmarek <[email protected]>
|
Addressed both follow-up findings in f7819ce. Linter source metadata is now stored as config paths, survives the language-server JSON cache, and is reparsed before linter diagnostics; |
There was a problem hiding this comment.
🔵 Needs a closer look
Tighten the diagnostic assertion in config.test.ts before approval.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
packages/compiler/test/config/config.test.ts:297
- This assertion searches for one matching diagnostic and throws only when none is found, so the test can still pass with extra diagnostics or an unexpected severity. Please use the shared
expectDiagnosticshelper to assert the exact diagnostic list (and then inspectprogram.diagnostics[0]for the source span), as required by the compiler test conventions.
- Files reviewed: 10/10 changed files
- Comments generated: 0 new
- Review effort level: Lite
Signed-off-by: Sylvester Kaczmarek <[email protected]>
|
Addressed the final test-convention review in 5f8affb. The fixture now isolates the inherited ruleset error and |
There was a problem hiding this comment.
🔵 Needs a closer look
Three unresolved moderate findings require addressing before approval.
Review details
Suppressed comments (3)
packages/compiler/src/core/program.ts:435
linterSource.extendsis recorded for every linterextendsvalue, so this rereads and reparses the declaring config on every compilation even when all references are library rulesets. That adds avoidable I/O and YAML parsing to each language-server compile; only load the source when the final ruleset contains afile:reference, which is the only path whereextendRuleSetconsumessource.
const linterSourcePath = options.configFile?.linterSource?.extends;
if (linterSourcePath) {
try {
const [script] = parseYaml(await host.readFile(linterSourcePath));
packages/compiler/src/core/program.ts:441
- The new failure path here has no regression test: the added integration test only covers successfully rereading the inherited config. Please add a compile test that makes the declaring config unreadable after config resolution and verifies compilation still reports the ruleset diagnostic via the documented fallback, so this race-handling branch cannot silently lose diagnostics or targets.
} catch {
// The config was readable when it was loaded. If it disappeared between
// config resolution and compilation, fall back to the final config source.
}
}
packages/compiler/test/config/config.test.ts:107
- This fixture only uses different keys for the parent and child (
enablevs.disable), so it would still pass if the implementation incorrectly deep-merged a single field's rule map. Add a case where both configs define the sameenableordisablefield and assert that the child replaces the entire parent map, as documented.
it("merges linter settings from an extended config", async () => {
const config = await loadTestConfig("extends-linter");
deepStrictEqual(config.linter, {
extends: ["test/all"],
enable: { "test/base-rule": true },
disable: { "test/child-rule": "Child exemption" },
});
- Files reviewed: 10/10 changed files
- Comments generated: 0 new
- Review effort level: Lite
Signed-off-by: Sylvester Kaczmarek <[email protected]>
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate diagnostic source-location issues remain.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
packages/compiler/src/core/cli/actions/info.ts:36
printInfoActionis the user-facing path, but the new test invokesgetPrintableConfigdirectly. If this call is removed or the action serializes the original config, the test still passes whiletsp infoleakslinterSource; coverprintInfoActionoutput (for example by capturingconsole.log) rather than only the helper.
const { filename, ...restOfConfig } = getPrintableConfig(config);
packages/compiler/src/core/program.ts:448
- When the inherited config cannot be reread, this fallback attaches the child YAML as the source even though the inherited
file:entry is not present underlinter.extendsin that file.getLocationInYamlScripttherefore returns the child file at offset 0, so the missing-ruleset diagnostic is reported at the wrong location; avoid falling back to the child source for this case (or synthesize a source forlinterSourcePath) and assert the target in the fallback test.
linterSource ??= options.configFile?.file
? { script: options.configFile.file, path: ["linter"] }
: undefined;
- Files reviewed: 10/10 changed files
- Comments generated: 1
- Review effort level: Lite
| const [script] = parseYaml(await host.readFile(linterSourcePath)); | ||
| linterSource = { script, path: ["linter"] }; |
|
Addressed the two actionable follow-ups in |
Summary
Fixes #11849.
Merge the
linterentry one level when a TypeSpec config extends another config. Child linter fields now override matching parent fields without dropping unrelated inherited settings such asextendsorenable.Testing
pnpm vitest run test/config/config.test.tsinpackages/compilerpnpm change verify