@intentius/chant-lexicon-gitlab 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/integrity.json +10 -6
  2. package/dist/manifest.json +1 -1
  3. package/dist/meta.json +186 -8
  4. package/dist/rules/wgl012.ts +86 -0
  5. package/dist/rules/wgl013.ts +62 -0
  6. package/dist/rules/wgl014.ts +51 -0
  7. package/dist/rules/wgl015.ts +85 -0
  8. package/dist/rules/yaml-helpers.ts +65 -3
  9. package/dist/skills/chant-gitlab.md +467 -24
  10. package/dist/types/index.d.ts +55 -16
  11. package/package.json +2 -2
  12. package/src/codegen/__snapshots__/snapshot.test.ts.snap +58 -0
  13. package/src/codegen/docs.ts +32 -9
  14. package/src/codegen/generate-lexicon.ts +6 -1
  15. package/src/codegen/generate.ts +45 -50
  16. package/src/codegen/naming.ts +3 -0
  17. package/src/codegen/parse.test.ts +154 -4
  18. package/src/codegen/parse.ts +161 -49
  19. package/src/codegen/snapshot.test.ts +7 -5
  20. package/src/composites/composites.test.ts +452 -0
  21. package/src/composites/docker-build.ts +81 -0
  22. package/src/composites/index.ts +8 -0
  23. package/src/composites/node-pipeline.ts +104 -0
  24. package/src/composites/python-pipeline.ts +75 -0
  25. package/src/composites/review-app.ts +63 -0
  26. package/src/generated/index.d.ts +55 -16
  27. package/src/generated/index.ts +3 -0
  28. package/src/generated/lexicon-gitlab.json +186 -8
  29. package/src/import/generator.ts +3 -2
  30. package/src/index.ts +4 -0
  31. package/src/lint/post-synth/wgl012.test.ts +131 -0
  32. package/src/lint/post-synth/wgl012.ts +86 -0
  33. package/src/lint/post-synth/wgl013.test.ts +164 -0
  34. package/src/lint/post-synth/wgl013.ts +62 -0
  35. package/src/lint/post-synth/wgl014.test.ts +97 -0
  36. package/src/lint/post-synth/wgl014.ts +51 -0
  37. package/src/lint/post-synth/wgl015.test.ts +139 -0
  38. package/src/lint/post-synth/wgl015.ts +85 -0
  39. package/src/lint/post-synth/yaml-helpers.ts +65 -3
  40. package/src/plugin.test.ts +39 -13
  41. package/src/plugin.ts +636 -40
  42. package/src/serializer.test.ts +140 -0
  43. package/src/serializer.ts +63 -5
  44. package/src/validate.ts +1 -0
  45. package/src/variables.ts +4 -0
@@ -0,0 +1,131 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
3
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
4
+ import { wgl012, checkDeprecatedProperties } from "./wgl012";
5
+
6
+ class MockEntity implements Declarable {
7
+ readonly [DECLARABLE_MARKER] = true as const;
8
+ readonly lexicon = "gitlab";
9
+ readonly entityType: string;
10
+ readonly kind = "resource" as const;
11
+ readonly props: Record<string, unknown>;
12
+
13
+ constructor(entityType: string, props: Record<string, unknown> = {}) {
14
+ this.entityType = entityType;
15
+ this.props = props;
16
+ }
17
+ }
18
+
19
+ function makeCtx(entities: Map<string, Declarable>): PostSynthContext {
20
+ return {
21
+ outputs: new Map(),
22
+ entities,
23
+ buildResult: {
24
+ outputs: new Map(),
25
+ entities,
26
+ warnings: [],
27
+ errors: [],
28
+ sourceFileCount: 1,
29
+ },
30
+ };
31
+ }
32
+
33
+ /** Synthetic deprecated-property map — no disk dependency. */
34
+ function fakeDeprecated(): Map<string, Set<string>> {
35
+ return new Map([
36
+ ["GitLab::CI::Job", new Set(["only", "except"])],
37
+ ["GitLab::CI::Artifacts", new Set(["license_management"])],
38
+ ]);
39
+ }
40
+
41
+ describe("WGL012: Deprecated Property Usage", () => {
42
+ test("check metadata", () => {
43
+ expect(wgl012.id).toBe("WGL012");
44
+ expect(wgl012.description).toContain("Deprecated");
45
+ });
46
+
47
+ test("emits warning for deprecated property", () => {
48
+ const entities = new Map<string, Declarable>([
49
+ ["deployJob", new MockEntity("GitLab::CI::Job", {
50
+ only: ["main"],
51
+ script: ["deploy.sh"],
52
+ })],
53
+ ]);
54
+ const diags = checkDeprecatedProperties(makeCtx(entities), fakeDeprecated());
55
+ expect(diags).toHaveLength(1);
56
+ expect(diags[0].checkId).toBe("WGL012");
57
+ expect(diags[0].severity).toBe("warning");
58
+ expect(diags[0].message).toContain("only");
59
+ expect(diags[0].message).toContain("deployJob");
60
+ expect(diags[0].message).toContain("deprecated");
61
+ expect(diags[0].entity).toBe("deployJob");
62
+ expect(diags[0].lexicon).toBe("gitlab");
63
+ });
64
+
65
+ test("emits one warning per deprecated property", () => {
66
+ const entities = new Map<string, Declarable>([
67
+ ["oldJob", new MockEntity("GitLab::CI::Job", {
68
+ only: ["main"],
69
+ except: ["tags"],
70
+ script: ["test"],
71
+ })],
72
+ ]);
73
+ const diags = checkDeprecatedProperties(makeCtx(entities), fakeDeprecated());
74
+ expect(diags).toHaveLength(2);
75
+ expect(diags.some((d) => d.message.includes("only"))).toBe(true);
76
+ expect(diags.some((d) => d.message.includes("except"))).toBe(true);
77
+ });
78
+
79
+ test("no diagnostic for non-deprecated properties", () => {
80
+ const entities = new Map<string, Declarable>([
81
+ ["testJob", new MockEntity("GitLab::CI::Job", {
82
+ script: ["npm test"],
83
+ stage: "test",
84
+ })],
85
+ ]);
86
+ const diags = checkDeprecatedProperties(makeCtx(entities), fakeDeprecated());
87
+ expect(diags).toHaveLength(0);
88
+ });
89
+
90
+ test("no diagnostic for entity type not in map", () => {
91
+ const entities = new Map<string, Declarable>([
92
+ ["myCache", new MockEntity("GitLab::CI::Cache", {
93
+ paths: ["node_modules/"],
94
+ })],
95
+ ]);
96
+ const diags = checkDeprecatedProperties(makeCtx(entities), fakeDeprecated());
97
+ expect(diags).toHaveLength(0);
98
+ });
99
+
100
+ test("no diagnostic on empty entities", () => {
101
+ const diags = checkDeprecatedProperties(makeCtx(new Map()), fakeDeprecated());
102
+ expect(diags).toHaveLength(0);
103
+ });
104
+
105
+ test("returns empty when deprecated map is empty", () => {
106
+ const entities = new Map<string, Declarable>([
107
+ ["job", new MockEntity("GitLab::CI::Job", { only: ["main"] })],
108
+ ]);
109
+ const diags = checkDeprecatedProperties(makeCtx(entities), new Map());
110
+ expect(diags).toHaveLength(0);
111
+ });
112
+
113
+ test("flags deprecated properties across multiple entities", () => {
114
+ const entities = new Map<string, Declarable>([
115
+ ["oldJob", new MockEntity("GitLab::CI::Job", { only: ["main"], script: ["test"] })],
116
+ ["artifacts", new MockEntity("GitLab::CI::Artifacts", { license_management: "report.json" })],
117
+ ]);
118
+ const diags = checkDeprecatedProperties(makeCtx(entities), fakeDeprecated());
119
+ expect(diags).toHaveLength(2);
120
+ expect(diags[0].entity).toBe("oldJob");
121
+ expect(diags[1].entity).toBe("artifacts");
122
+ });
123
+
124
+ test("handles entity with no props", () => {
125
+ const entities = new Map<string, Declarable>([
126
+ ["emptyJob", new MockEntity("GitLab::CI::Job")],
127
+ ]);
128
+ const diags = checkDeprecatedProperties(makeCtx(entities), fakeDeprecated());
129
+ expect(diags).toHaveLength(0);
130
+ });
131
+ });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * WGL012: Deprecated Property Usage
3
+ *
4
+ * Flags properties marked as deprecated in the GitLab CI schema.
5
+ * Sources: description text mining (keywords like "Deprecated", "legacy").
6
+ */
7
+
8
+ import { readFileSync } from "fs";
9
+ import { join } from "path";
10
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
11
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
12
+
13
+ interface LexiconEntry {
14
+ kind: string;
15
+ resourceType: string;
16
+ deprecatedProperties?: string[];
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ /**
21
+ * Load deprecated properties per entity type from the lexicon JSON.
22
+ */
23
+ function loadDeprecatedProperties(): Map<string, Set<string>> {
24
+ const map = new Map<string, Set<string>>();
25
+ try {
26
+ const pkgDir = join(__dirname, "..", "..", "..");
27
+ const lexiconPath = join(pkgDir, "src", "generated", "lexicon-gitlab.json");
28
+ const content = readFileSync(lexiconPath, "utf-8");
29
+ const data = JSON.parse(content) as Record<string, LexiconEntry>;
30
+
31
+ for (const [_name, entry] of Object.entries(data)) {
32
+ if (entry.resourceType && entry.deprecatedProperties && entry.deprecatedProperties.length > 0) {
33
+ map.set(entry.resourceType, new Set(entry.deprecatedProperties));
34
+ }
35
+ }
36
+ } catch {
37
+ // Lexicon not available — skip
38
+ }
39
+ return map;
40
+ }
41
+
42
+ /**
43
+ * Core detection logic — exported for direct testing with synthetic data.
44
+ */
45
+ export function checkDeprecatedProperties(
46
+ ctx: PostSynthContext,
47
+ deprecated: Map<string, Set<string>>,
48
+ ): PostSynthDiagnostic[] {
49
+ if (deprecated.size === 0) return [];
50
+
51
+ const diagnostics: PostSynthDiagnostic[] = [];
52
+
53
+ for (const [entityName, entity] of ctx.entities) {
54
+ if (isPropertyDeclarable(entity)) continue;
55
+
56
+ const entityType = (entity as Record<string, unknown>).entityType as string;
57
+ const deprProps = deprecated.get(entityType);
58
+ if (!deprProps) continue;
59
+
60
+ const props = (entity as Record<string, unknown>).props as Record<string, unknown> | undefined;
61
+ if (!props) continue;
62
+
63
+ for (const propName of Object.keys(props)) {
64
+ if (deprProps.has(propName)) {
65
+ diagnostics.push({
66
+ checkId: "WGL012",
67
+ severity: "warning",
68
+ message: `Entity "${entityName}" (${entityType}) uses deprecated property "${propName}" — consider alternatives`,
69
+ entity: entityName,
70
+ lexicon: "gitlab",
71
+ });
72
+ }
73
+ }
74
+ }
75
+
76
+ return diagnostics;
77
+ }
78
+
79
+ export const wgl012: PostSynthCheck = {
80
+ id: "WGL012",
81
+ description: "Deprecated property usage — flags properties marked as deprecated in the GitLab CI schema",
82
+
83
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
84
+ return checkDeprecatedProperties(ctx, loadDeprecatedProperties());
85
+ },
86
+ };
@@ -0,0 +1,164 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
3
+ import { wgl013, checkInvalidNeeds } from "./wgl013";
4
+
5
+ function makeCtx(yaml: string): PostSynthContext {
6
+ return {
7
+ outputs: new Map([["gitlab", yaml]]),
8
+ entities: new Map(),
9
+ buildResult: {
10
+ outputs: new Map([["gitlab", yaml]]),
11
+ entities: new Map(),
12
+ warnings: [],
13
+ errors: [],
14
+ sourceFileCount: 1,
15
+ },
16
+ };
17
+ }
18
+
19
+ describe("WGL013: Invalid needs: Target", () => {
20
+ test("check metadata", () => {
21
+ expect(wgl013.id).toBe("WGL013");
22
+ expect(wgl013.description).toContain("needs:");
23
+ });
24
+
25
+ test("dangling needs target → error", () => {
26
+ const yaml = `stages:
27
+ - build
28
+ - test
29
+
30
+ build:
31
+ stage: build
32
+ script:
33
+ - npm run build
34
+
35
+ test:
36
+ stage: test
37
+ needs:
38
+ - nonexistent
39
+ script:
40
+ - npm test`;
41
+ const diags = checkInvalidNeeds(makeCtx(yaml));
42
+ expect(diags).toHaveLength(1);
43
+ expect(diags[0].checkId).toBe("WGL013");
44
+ expect(diags[0].severity).toBe("error");
45
+ expect(diags[0].message).toContain("nonexistent");
46
+ expect(diags[0].message).toContain("not defined");
47
+ expect(diags[0].entity).toBe("test");
48
+ expect(diags[0].lexicon).toBe("gitlab");
49
+ });
50
+
51
+ test("self-referencing needs → error", () => {
52
+ const yaml = `stages:
53
+ - build
54
+
55
+ build:
56
+ stage: build
57
+ needs:
58
+ - build
59
+ script:
60
+ - npm run build`;
61
+ const diags = checkInvalidNeeds(makeCtx(yaml));
62
+ expect(diags).toHaveLength(1);
63
+ expect(diags[0].checkId).toBe("WGL013");
64
+ expect(diags[0].severity).toBe("error");
65
+ expect(diags[0].message).toContain("itself");
66
+ expect(diags[0].entity).toBe("build");
67
+ });
68
+
69
+ test("valid needs → no diagnostic", () => {
70
+ const yaml = `stages:
71
+ - build
72
+ - test
73
+
74
+ build:
75
+ stage: build
76
+ script:
77
+ - npm run build
78
+
79
+ test:
80
+ stage: test
81
+ needs:
82
+ - build
83
+ script:
84
+ - npm test`;
85
+ const diags = checkInvalidNeeds(makeCtx(yaml));
86
+ expect(diags).toHaveLength(0);
87
+ });
88
+
89
+ test("no needs → no diagnostic", () => {
90
+ const yaml = `stages:
91
+ - build
92
+
93
+ build:
94
+ stage: build
95
+ script:
96
+ - npm run build`;
97
+ const diags = checkInvalidNeeds(makeCtx(yaml));
98
+ expect(diags).toHaveLength(0);
99
+ });
100
+
101
+ test("multiple jobs, only one with bad needs → 1 diagnostic", () => {
102
+ const yaml = `stages:
103
+ - build
104
+ - test
105
+ - deploy
106
+
107
+ build:
108
+ stage: build
109
+ script:
110
+ - npm run build
111
+
112
+ test:
113
+ stage: test
114
+ needs:
115
+ - build
116
+ script:
117
+ - npm test
118
+
119
+ deploy:
120
+ stage: deploy
121
+ needs:
122
+ - nonexistent
123
+ script:
124
+ - ./deploy.sh`;
125
+ const diags = checkInvalidNeeds(makeCtx(yaml));
126
+ expect(diags).toHaveLength(1);
127
+ expect(diags[0].entity).toBe("deploy");
128
+ expect(diags[0].message).toContain("nonexistent");
129
+ });
130
+
131
+ test("skips check when include: is present", () => {
132
+ const yaml = `include:
133
+ - local: .gitlab/ci/templates.yml
134
+
135
+ stages:
136
+ - test
137
+
138
+ test:
139
+ stage: test
140
+ needs:
141
+ - from-included-file
142
+ script:
143
+ - npm test`;
144
+ const diags = checkInvalidNeeds(makeCtx(yaml));
145
+ expect(diags).toHaveLength(0);
146
+ });
147
+
148
+ test("multiple invalid needs → multiple diagnostics", () => {
149
+ const yaml = `stages:
150
+ - build
151
+
152
+ build:
153
+ stage: build
154
+ needs:
155
+ - ghost1
156
+ - ghost2
157
+ script:
158
+ - npm run build`;
159
+ const diags = checkInvalidNeeds(makeCtx(yaml));
160
+ expect(diags).toHaveLength(2);
161
+ expect(diags[0].message).toContain("ghost1");
162
+ expect(diags[1].message).toContain("ghost2");
163
+ });
164
+ });
@@ -0,0 +1,62 @@
1
+ /**
2
+ * WGL013: Invalid `needs:` Target
3
+ *
4
+ * Detects two cases in the serialized YAML:
5
+ * - Dangling reference: `needs:` names a job not defined in the pipeline
6
+ * - Self-reference: job lists itself in `needs:`
7
+ *
8
+ * Both cause GitLab pipeline validation failures.
9
+ *
10
+ * Caveat: when `include:` is present, referenced jobs may come from
11
+ * included files, so the check is skipped.
12
+ */
13
+
14
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
15
+ import { getPrimaryOutput, extractJobs, hasInclude } from "./yaml-helpers";
16
+
17
+ export function checkInvalidNeeds(ctx: PostSynthContext): PostSynthDiagnostic[] {
18
+ const diagnostics: PostSynthDiagnostic[] = [];
19
+
20
+ for (const [, output] of ctx.outputs) {
21
+ const yaml = getPrimaryOutput(output);
22
+ if (hasInclude(yaml)) continue;
23
+
24
+ const jobs = extractJobs(yaml);
25
+ const jobNames = new Set(jobs.keys());
26
+
27
+ for (const [jobName, job] of jobs) {
28
+ if (!job.needs) continue;
29
+
30
+ for (const need of job.needs) {
31
+ if (need === jobName) {
32
+ diagnostics.push({
33
+ checkId: "WGL013",
34
+ severity: "error",
35
+ message: `Job "${jobName}" lists itself in needs: — self-references are invalid`,
36
+ entity: jobName,
37
+ lexicon: "gitlab",
38
+ });
39
+ } else if (!jobNames.has(need)) {
40
+ diagnostics.push({
41
+ checkId: "WGL013",
42
+ severity: "error",
43
+ message: `Job "${jobName}" needs "${need}" which is not defined in the pipeline`,
44
+ entity: jobName,
45
+ lexicon: "gitlab",
46
+ });
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ return diagnostics;
53
+ }
54
+
55
+ export const wgl013: PostSynthCheck = {
56
+ id: "WGL013",
57
+ description: "Invalid needs: target — dangling reference or self-reference",
58
+
59
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
60
+ return checkInvalidNeeds(ctx);
61
+ },
62
+ };
@@ -0,0 +1,97 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
3
+ import { wgl014, checkInvalidExtends } from "./wgl014";
4
+
5
+ function makeCtx(yaml: string): PostSynthContext {
6
+ return {
7
+ outputs: new Map([["gitlab", yaml]]),
8
+ entities: new Map(),
9
+ buildResult: {
10
+ outputs: new Map([["gitlab", yaml]]),
11
+ entities: new Map(),
12
+ warnings: [],
13
+ errors: [],
14
+ sourceFileCount: 1,
15
+ },
16
+ };
17
+ }
18
+
19
+ describe("WGL014: Invalid extends: Target", () => {
20
+ test("check metadata", () => {
21
+ expect(wgl014.id).toBe("WGL014");
22
+ expect(wgl014.description).toContain("extends:");
23
+ });
24
+
25
+ test("extends nonexistent template → error", () => {
26
+ const yaml = `stages:
27
+ - deploy
28
+
29
+ deploy:
30
+ stage: deploy
31
+ extends: .nonexistent-template
32
+ script:
33
+ - ./deploy.sh`;
34
+ const diags = checkInvalidExtends(makeCtx(yaml));
35
+ expect(diags).toHaveLength(1);
36
+ expect(diags[0].checkId).toBe("WGL014");
37
+ expect(diags[0].severity).toBe("error");
38
+ expect(diags[0].message).toContain(".nonexistent-template");
39
+ expect(diags[0].message).toContain("not defined");
40
+ expect(diags[0].entity).toBe("deploy");
41
+ expect(diags[0].lexicon).toBe("gitlab");
42
+ });
43
+
44
+ test("extends existing hidden job → no diagnostic", () => {
45
+ const yaml = `.deploy-template:
46
+ image: alpine
47
+ script:
48
+ - echo "template"
49
+
50
+ deploy:
51
+ stage: deploy
52
+ extends: .deploy-template
53
+ script:
54
+ - ./deploy.sh`;
55
+ const diags = checkInvalidExtends(makeCtx(yaml));
56
+ expect(diags).toHaveLength(0);
57
+ });
58
+
59
+ test("no extends → no diagnostic", () => {
60
+ const yaml = `stages:
61
+ - build
62
+
63
+ build:
64
+ stage: build
65
+ script:
66
+ - npm run build`;
67
+ const diags = checkInvalidExtends(makeCtx(yaml));
68
+ expect(diags).toHaveLength(0);
69
+ });
70
+
71
+ test("skips check when include: is present", () => {
72
+ const yaml = `include:
73
+ - local: .gitlab/ci/templates.yml
74
+
75
+ deploy:
76
+ stage: deploy
77
+ extends: .deploy-template
78
+ script:
79
+ - ./deploy.sh`;
80
+ const diags = checkInvalidExtends(makeCtx(yaml));
81
+ expect(diags).toHaveLength(0);
82
+ });
83
+
84
+ test("multiple extends targets, one invalid → 1 diagnostic", () => {
85
+ const yaml = `.base:
86
+ image: alpine
87
+
88
+ deploy:
89
+ stage: deploy
90
+ extends: [.base, .missing]
91
+ script:
92
+ - ./deploy.sh`;
93
+ const diags = checkInvalidExtends(makeCtx(yaml));
94
+ expect(diags).toHaveLength(1);
95
+ expect(diags[0].message).toContain(".missing");
96
+ });
97
+ });
@@ -0,0 +1,51 @@
1
+ /**
2
+ * WGL014: Invalid `extends:` Target
3
+ *
4
+ * Detects jobs that `extends:` a template not defined in the pipeline YAML.
5
+ * GitLab rejects pipelines with unresolved extends references.
6
+ *
7
+ * Caveat: when `include:` is present, templates may come from
8
+ * included files, so the check is skipped.
9
+ */
10
+
11
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
12
+ import { getPrimaryOutput, extractJobs, hasInclude } from "./yaml-helpers";
13
+
14
+ export function checkInvalidExtends(ctx: PostSynthContext): PostSynthDiagnostic[] {
15
+ const diagnostics: PostSynthDiagnostic[] = [];
16
+
17
+ for (const [, output] of ctx.outputs) {
18
+ const yaml = getPrimaryOutput(output);
19
+ if (hasInclude(yaml)) continue;
20
+
21
+ const jobs = extractJobs(yaml);
22
+ const jobNames = new Set(jobs.keys());
23
+
24
+ for (const [jobName, job] of jobs) {
25
+ if (!job.extends) continue;
26
+
27
+ for (const target of job.extends) {
28
+ if (!jobNames.has(target)) {
29
+ diagnostics.push({
30
+ checkId: "WGL014",
31
+ severity: "error",
32
+ message: `Job "${jobName}" extends "${target}" which is not defined in the pipeline`,
33
+ entity: jobName,
34
+ lexicon: "gitlab",
35
+ });
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ return diagnostics;
42
+ }
43
+
44
+ export const wgl014: PostSynthCheck = {
45
+ id: "WGL014",
46
+ description: "Invalid extends: target — references a template not in the pipeline",
47
+
48
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
49
+ return checkInvalidExtends(ctx);
50
+ },
51
+ };