Skip to content

Commit 4b560e1

Browse files
project .pliplugin program config must win over user-scope pli.pgm_conf (#803)
project .pliplugin program config wins over user-scope pli.pgm_conf Signed-off-by: Wagner Laranjeiras <wagner.laranjeiras@typefox.io>
1 parent 83a5189 commit 4b560e1

4 files changed

Lines changed: 253 additions & 26 deletions

File tree

packages/language/src/config/schema.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ export interface GroupRecord extends ProcessGroup {
105105
computedLibs: LibsEntry[];
106106
}
107107

108+
/**
109+
* Where a {@link ProgramRecord} was loaded from. Project (`.pliplugin/`)
110+
* entries take precedence over user-scope `settings.json` entries when a file
111+
* matches program configs from both sources — see `getProgramConfig`.
112+
*/
113+
export type ProgramOrigin = "project" | "settings";
114+
108115
/**
109116
* A {@link ProgramConfig} together with its merged compiler options. The
110117
* pli-options strings (this config's plus the bound process group's) are
@@ -118,6 +125,12 @@ export interface ProgramRecord extends ProgramConfig {
118125
* compiler-options translator doesn't double-report them.
119126
*/
120127
issues: Diagnostic[];
128+
/**
129+
* Configuration source this record came from. Absent is treated as
130+
* `"project"` (highest precedence) so non-merge callers (quick-fix add,
131+
* tests, `.pliplugin/`-only parse) keep binding at project precedence.
132+
*/
133+
origin?: ProgramOrigin;
121134
}
122135

123136
/**

packages/language/src/workspace/plugin-configuration-provider.ts

Lines changed: 119 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
plainItem,
3636
ProcessGroup,
3737
ProgramConfig,
38+
ProgramOrigin,
3839
ProgramRecord,
3940
} from "../config/schema";
4041
import { isBoolean, isNumber, isStringArray } from "../utils/types";
@@ -65,6 +66,7 @@ export {
6566
type ProcessGroup,
6667
type ProgramConfig,
6768
type ProgramEntry,
69+
type ProgramOrigin,
6870
type ProgramRecord,
6971
} from "../config/schema";
7072

@@ -186,6 +188,26 @@ interface ConfigSource {
186188
document: TextDocument;
187189
}
188190

191+
/**
192+
* Precedence of a program-config match against a file, **lowest value wins**.
193+
* Used by {@link PluginConfigurationProvider.getProgramConfig} to pick the
194+
* best match rather than the first hit, so a user-scope `settings.json` entry
195+
* can never shadow a project `.pliplugin/` entry that also matches:
196+
*
197+
* 1. source — project (`.pliplugin/`) beats settings;
198+
* 2. specificity — an exact path beats a glob (within the same source).
199+
*
200+
* Ties beyond this are broken by insertion order (project entries are merged
201+
* first). `ProjectExact` is the best possible rank, so matching it lets the
202+
* lookup stop early.
203+
*/
204+
const ProgramMatchRank = {
205+
ProjectExact: 0,
206+
ProjectGlob: 1,
207+
SettingsExact: 2,
208+
SettingsGlob: 3,
209+
} as const;
210+
189211
/**
190212
* Plugin configuration provider for loading '.pliplugin/pgm_conf.json' and '.pliplugin/proc_grps.json' (when they exist),
191213
* processing their contents, and making those settings available to the language server.
@@ -473,23 +495,44 @@ export class PluginConfigurationProvider {
473495
(source): source is ConfigSource => source !== undefined,
474496
);
475497

476-
// Parse pgm_conf sources and merge by resolved program URI.
477-
const mergedPrograms = new Map<string, ProgramConfig>();
478-
for (const source of pgmSources) {
498+
// Parse pgm_conf sources and merge by resolved program URI, tagging each
499+
// entry with its origin (project `.pliplugin/` vs. user-scope settings).
500+
//
501+
// Two layers of precedence work together:
502+
// 1. Here, `.pliplugin/` sources are processed FIRST (the reverse of
503+
// `pgmSources`) and the `has` guard keeps the first entry per key, so a
504+
// project entry wins any exact-key collision with settings.
505+
// 2. `getProgramConfig` then prefers project entries over settings entries
506+
// at lookup time (see its ranking). Together these guarantee a settings
507+
// program entry can never shadow a project entry that also matches the
508+
// file — whether the settings entry is a broad glob (e.g. `**/*`) or an
509+
// exact path (e.g. `a.pli`, which a plain `Map.get` would otherwise let
510+
// win via a direct key hit).
511+
const projectPgmConfUri = plipluginPgmConfUri.toString();
512+
const mergedPrograms = new Map<
513+
string,
514+
{ config: ProgramConfig; origin: ProgramOrigin }
515+
>();
516+
for (const source of [...pgmSources].reverse()) {
479517
this.knownPgmConfUris.add(source.uri.toString());
480518
const result = parseProgramConfigs(source.text, source.uri, source.entry);
481519
diagnostics.push(...result.diagnostics);
482520
if (result.config) {
521+
const origin: ProgramOrigin =
522+
source.uri.toString() === projectPgmConfUri ? "project" : "settings";
483523
for (const config of result.config) {
484524
const resolvedUri = this.resolveProgramPath(
485525
config.program.value,
486526
this.workspacePath,
487527
);
488-
mergedPrograms.set(resolvedUri.toString(), config);
528+
const key = resolvedUri.toString();
529+
if (!mergedPrograms.has(key)) {
530+
mergedPrograms.set(key, { config, origin });
531+
}
489532
}
490533
}
491534
}
492-
this.setProgramConfigs(
535+
this.applyProgramConfigs(
493536
this.workspacePath,
494537
Array.from(mergedPrograms.values()),
495538
);
@@ -866,10 +909,28 @@ export class PluginConfigurationProvider {
866909
public setProgramConfigs(
867910
workspaceUri: URI,
868911
programConfigs: ProgramConfig[],
912+
): void {
913+
// Direct callers (quick-fix add, `.pliplugin/`-only parse, tests) are all
914+
// project-scoped, so their entries bind at project precedence.
915+
this.applyProgramConfigs(
916+
workspaceUri,
917+
programConfigs.map((config) => ({ config, origin: "project" as const })),
918+
);
919+
}
920+
921+
/**
922+
* Rebuilds the program-config map from the given entries, each tagged with
923+
* the source it came from ({@link ProgramOrigin}). Program paths are
924+
* normalized and resolved relative to the workspace (unless absolute), then
925+
* post-processed so abstract options are built.
926+
*/
927+
private applyProgramConfigs(
928+
workspaceUri: URI,
929+
entries: Array<{ config: ProgramConfig; origin: ProgramOrigin }>,
869930
): void {
870931
this.programConfigs.clear();
871932

872-
for (const config of programConfigs) {
933+
for (const { config, origin } of entries) {
873934
const resolvedUri = this.resolveProgramPath(
874935
config.program.value,
875936
workspaceUri,
@@ -881,6 +942,7 @@ export class PluginConfigurationProvider {
881942
...config,
882943
abstractOptions: { options: [], tokens: [], issues: [], comments: [] },
883944
issues: [],
945+
origin,
884946
});
885947
}
886948
this.postProcessProgramConfigs();
@@ -996,11 +1058,14 @@ export class PluginConfigurationProvider {
9961058

9971059
/**
9981060
* Returns the program config for the given program URI.
999-
* Lookup order:
1000-
* 1. Exact match against registered config keys.
1001-
* 2. Glob pattern match (using minimatch) against decoded URIs.
10021061
*
1003-
* @see https://github.com/isaacs/minimatch
1062+
* Rather than returning the first hit, this picks the *highest-precedence*
1063+
* match (see {@link ProgramMatchRank}) so a user-scope `settings.json` entry
1064+
* can never shadow a project `.pliplugin/` entry that also matches. This is
1065+
* what fixes the exact-path settings case: a plain `Map.get(uri)` direct hit
1066+
* would return a settings `program: "a.pli"` entry even when a project glob
1067+
* (e.g. `*.pli`) also matches; ranking lets the project glob win instead.
1068+
*
10041069
* @param program Name of the program to get a config for
10051070
* @returns Associated program config, or undefined if not found
10061071
*/
@@ -1010,32 +1075,60 @@ export class PluginConfigurationProvider {
10101075
// the path alone is sufficient.
10111076
// Note that we need to decode the URI
10121077
const uri = program.toString(true);
1013-
const direct = this.programConfigs.get(uri);
1014-
if (direct) {
1015-
return direct;
1016-
}
1017-
// fallback to glob matching
1078+
1079+
let best: ProgramRecord | undefined;
1080+
let bestRank = Number.POSITIVE_INFINITY;
10181081
for (const [pattern, config] of this.programConfigs.entries()) {
1019-
if (pattern === uri) {
1020-
continue; // already checked
1082+
const rank = this.programMatchRank(uri, pattern, config);
1083+
if (rank === undefined || rank >= bestRank) {
1084+
continue; // no match, or not better than what we already have
10211085
}
1086+
best = config;
1087+
bestRank = rank;
1088+
if (rank === ProgramMatchRank.ProjectExact) {
1089+
break; // best possible rank
1090+
}
1091+
}
1092+
return best;
1093+
}
1094+
1095+
/**
1096+
* Precedence of a single program-config entry as a match for `uri`, or
1097+
* `undefined` when its pattern does not match the file. Lower ranks win;
1098+
* see {@link ProgramMatchRank}.
1099+
*
1100+
* @param uri Decoded program URI being resolved.
1101+
* @param pattern The program config's key (an exact path or a glob).
1102+
* @param record The candidate program config.
1103+
*/
1104+
private programMatchRank(
1105+
uri: string,
1106+
pattern: string,
1107+
record: ProgramRecord,
1108+
): number | undefined {
1109+
const isExact = pattern === uri;
1110+
if (!isExact) {
10221111
try {
10231112
// attempt match on decoded URI
1024-
if (
1025-
minimatch(uri, decodeURIComponent(pattern), {
1026-
nocase: true,
1027-
})
1028-
) {
1029-
return config;
1113+
if (!minimatch(uri, decodeURIComponent(pattern), { nocase: true })) {
1114+
return undefined;
10301115
}
10311116
} catch (e) {
10321117
console.error(
1033-
`Invalid glob pattern "${pattern}" for program "${program}": ${e}`,
1118+
`Invalid glob pattern "${pattern}" for program "${uri}": ${e}`,
10341119
);
1120+
return undefined;
10351121
}
10361122
}
1037-
// no match
1038-
return undefined;
1123+
// Absent origin is treated as "project" (see ProgramRecord.origin).
1124+
if (record.origin === "settings") {
1125+
return isExact
1126+
? ProgramMatchRank.SettingsExact
1127+
: ProgramMatchRank.SettingsGlob;
1128+
}
1129+
return isExact
1130+
? ProgramMatchRank.ProjectExact
1131+
: ProgramMatchRank.ProjectGlob;
10391132
}
10401133

10411134
/**
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* This program and the accompanying materials are made available under the terms of the
3+
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
4+
* https://www.eclipse.org/legal/epl-v20.html
5+
*
6+
* SPDX-License-Identifier: EPL-2.0
7+
*
8+
* Copyright Contributors to the Zowe Project.
9+
*
10+
*/
11+
12+
// A project `.pliplugin/pgm_conf.json` entry must win over a user-scope
13+
// `pli.pgm_conf` entry even when the settings entry is an EXACT path match for
14+
// the file. Here the settings bind `main.pli` (exact) to `DFLT` (no libs),
15+
// while the project binds `*.pli` (glob) to `default` (has the `cpyla` lib).
16+
//
17+
// This is the harder sibling of `program-config-precedence-over-settings.ts`:
18+
// a plain `Map.get(uri)` direct lookup would return the settings exact-path
19+
// entry and the include would fail with IBM1848I. `getProgramConfig` instead
20+
// ranks project matches above settings matches, so the project glob wins and
21+
// the include resolves.
22+
23+
/// <reference path="../../framework.ts" />
24+
25+
// @filename: .vscode/settings.json
26+
//// {
27+
//// "pli.pgm_conf": { "pgms": [{ "pgroup": "DFLT", "program": "main.pli" }] },
28+
//// "pli.proc_grps": { "pgroups": [{ "name": "DFLT", "libs": [], "include-extensions": [".pli"] }] }
29+
//// }
30+
31+
// @filename: .pliplugin/pgm_conf.json
32+
//// { "pgms": [{ "program": "*.pli", "pgroup": "default" }] }
33+
34+
// @filename: .pliplugin/proc_grps.json
35+
//// {
36+
//// "pgroups": [
37+
//// {
38+
//// "name": "default",
39+
//// "libs": [<|missing:"cpy"|>, "cpyla"],
40+
//// "include-extensions": [".pli"]
41+
//// }
42+
//// ]
43+
//// }
44+
45+
// @filename: cpyla/b.pli
46+
//// DECLARE LIB_VAR FIXED;
47+
48+
// @filename: main.pli
49+
//// %INCLUDE "b.pli";
50+
51+
// The include resolves via the project `default` group (which lists `cpyla`),
52+
// not the settings `DFLT` group that exact-matches `main.pli` with no libs.
53+
preprocessor.expectTokens(`
54+
DECLARE LIB_VAR FIXED;
55+
`);
56+
57+
// The project `default` group is the active source: its missing `cpy` lib is
58+
// flagged on proc_grps.json, while `cpyla` resolves.
59+
verify.expectDiagnosticsAt("missing", {
60+
message: code.LSP.PluginConfiguration.UnresolvedEntry.message("cpy"),
61+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* This program and the accompanying materials are made available under the terms of the
3+
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
4+
* https://www.eclipse.org/legal/epl-v20.html
5+
*
6+
* SPDX-License-Identifier: EPL-2.0
7+
*
8+
* Copyright Contributors to the Zowe Project.
9+
*
10+
*/
11+
12+
// A project `.pliplugin/pgm_conf.json` entry must win over a broader user-scope
13+
// `pli.pgm_conf` glob when binding a file to its process group. Here the user
14+
// settings bind every file (`**/*`) to `DFLT` (which has no libs), while the
15+
// project binds `*.pli` to `default` (which has the `cpyla` lib). `main.pli`
16+
// must resolve against `default`, so the include is found — otherwise it would
17+
// pick the empty settings group and fail with IBM1848I.
18+
//
19+
// Regression test for the include that "could not be found" even though the
20+
// resolvable lib was listed in the project proc_grps.json.
21+
22+
/// <reference path="../../framework.ts" />
23+
24+
// @filename: .vscode/settings.json
25+
//// {
26+
//// "pli.pgm_conf": { "pgms": [{ "pgroup": "DFLT", "program": "**/*" }] },
27+
//// "pli.proc_grps": { "pgroups": [{ "name": "DFLT", "libs": [], "include-extensions": [".pli"] }] }
28+
//// }
29+
30+
// @filename: .pliplugin/pgm_conf.json
31+
//// { "pgms": [{ "program": "*.pli", "pgroup": "default" }] }
32+
33+
// @filename: .pliplugin/proc_grps.json
34+
//// {
35+
//// "pgroups": [
36+
//// {
37+
//// "name": "default",
38+
//// "libs": [<|missing:"cpy"|>, "cpyla"],
39+
//// "include-extensions": [".pli"]
40+
//// }
41+
//// ]
42+
//// }
43+
44+
// @filename: cpyla/b.pli
45+
//// DECLARE LIB_VAR FIXED;
46+
47+
// @filename: main.pli
48+
//// %INCLUDE "b.pli";
49+
50+
// The include resolves via the project `default` group (which lists `cpyla`),
51+
// not the settings `DFLT` group (which has no libs).
52+
preprocessor.expectTokens(`
53+
DECLARE LIB_VAR FIXED;
54+
`);
55+
56+
// The project `default` group is the active source: its missing `cpy` lib is
57+
// flagged on proc_grps.json, while `cpyla` resolves.
58+
verify.expectDiagnosticsAt("missing", {
59+
message: code.LSP.PluginConfiguration.UnresolvedEntry.message("cpy"),
60+
});

0 commit comments

Comments
 (0)