Skip to content

Commit 28ee487

Browse files
committed
refactor(ui5Types/host): Improve isolated usage of host and add tests
This allows to create a host with the right compiler options which is helpful to write unit tests for TypeScript AST functionality without having to run the full linter.
1 parent bb968bb commit 28ee487

File tree

3 files changed

+204
-45
lines changed

3 files changed

+204
-45
lines changed

src/linter/ui5Types/TypeLinter.ts

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,39 +16,6 @@ import {JSONSchemaForSAPUI5Namespace} from "../../manifest.js";
1616

1717
const log = getLogger("linter:ui5Types:TypeLinter");
1818

19-
const DEFAULT_OPTIONS: ts.CompilerOptions = {
20-
target: ts.ScriptTarget.ES2022,
21-
module: ts.ModuleKind.ES2022,
22-
// Skip lib check to speed up linting. Libs should generally be fine,
23-
// we might want to add a unit test doing the check during development
24-
skipLibCheck: true,
25-
// Include standard typescript libraries for ES2022 and DOM support
26-
lib: ["lib.es2022.d.ts", "lib.dom.d.ts"],
27-
// Disable lib replacement lookup as we don't rely on it
28-
libReplacement: false,
29-
// Allow and check JavaScript files since this is everything we'll do here
30-
allowJs: true,
31-
checkJs: false,
32-
strict: true,
33-
noImplicitAny: false,
34-
strictNullChecks: false,
35-
strictPropertyInitialization: false,
36-
rootDir: "/",
37-
// Library modules (e.g. sap/ui/core/library.js) do not have a default export but
38-
// instead have named exports e.g. for enums defined in the module.
39-
// However, in current JavaScript code (UI5 AMD) the whole object is exported, as there are no
40-
// named exports outside of ES Modules / TypeScript.
41-
// This property compensates this gap and tries to all usage of default imports where actually
42-
// no default export is defined.
43-
// NOTE: This setting should not be used when analyzing TypeScript code, as it would allow
44-
// using an default import on library modules, which is not intended.
45-
// A better solution:
46-
// During transpilation, for every library module (where no default export exists),
47-
// an "import * as ABC" instead of a default import is created.
48-
// This logic needs to be in sync with the generator for UI5 TypeScript definitions.
49-
allowSyntheticDefaultImports: true,
50-
};
51-
5219
export default class TypeLinter {
5320
#sharedLanguageService: SharedLanguageService;
5421
#compilerOptions: ts.CompilerOptions;
@@ -69,7 +36,7 @@ export default class TypeLinter {
6936
this.#workspace = workspace;
7037
this.#filePathsWorkspace = filePathsWorkspace;
7138
this.#libraryDependencies = libraryDependencies;
72-
this.#compilerOptions = {...DEFAULT_OPTIONS};
39+
this.#compilerOptions = {};
7340

7441
const namespace = context.getNamespace();
7542
if (namespace) {

src/linter/ui5Types/host.ts

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,39 @@ function addSapui5TypesMappingToCompilerOptions(
8080
});
8181
}
8282

83+
const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = {
84+
target: ts.ScriptTarget.ES2022,
85+
module: ts.ModuleKind.ES2022,
86+
// Skip lib check to speed up linting. Libs should generally be fine,
87+
// we might want to add a unit test doing the check during development
88+
skipLibCheck: true,
89+
// Include standard typescript libraries for ES2022 and DOM support
90+
lib: ["lib.es2022.d.ts", "lib.dom.d.ts"],
91+
// Disable lib replacement lookup as we don't rely on it
92+
libReplacement: false,
93+
// Allow and check JavaScript files since this is everything we'll do here
94+
allowJs: true,
95+
checkJs: false,
96+
strict: true,
97+
noImplicitAny: false,
98+
strictNullChecks: false,
99+
strictPropertyInitialization: false,
100+
rootDir: "/",
101+
// Library modules (e.g. sap/ui/core/library.js) do not have a default export but
102+
// instead have named exports e.g. for enums defined in the module.
103+
// However, in current JavaScript code (UI5 AMD) the whole object is exported, as there are no
104+
// named exports outside of ES Modules / TypeScript.
105+
// This property compensates this gap and tries to all usage of default imports where actually
106+
// no default export is defined.
107+
// NOTE: This setting should not be used when analyzing TypeScript code, as it would allow
108+
// using an default import on library modules, which is not intended.
109+
// A better solution:
110+
// During transpilation, for every library module (where no default export exists),
111+
// an "import * as ABC" instead of a default import is created.
112+
// This logic needs to be in sync with the generator for UI5 TypeScript definitions.
113+
allowSyntheticDefaultImports: true,
114+
};
115+
83116
export type FileContents = Map<ResourcePath, string>;
84117

85118
export async function createVirtualLanguageServiceHost(
@@ -89,10 +122,15 @@ export async function createVirtualLanguageServiceHost(
89122
projectScriptVersion: string,
90123
libraryDependencies: JSONSchemaForSAPUI5Namespace["dependencies"]["libs"]
91124
): Promise<ts.LanguageServiceHost> {
125+
const compilerOptions = {
126+
...DEFAULT_COMPILER_OPTIONS,
127+
...options,
128+
};
129+
92130
const silly = log.isLevelEnabled("silly");
93131

94-
options.typeRoots = ["/types"];
95-
options.types = [];
132+
compilerOptions.typeRoots = ["/types"];
133+
compilerOptions.types = [];
96134

97135
const typePathMappings = new Map<string, string>();
98136
addPathMappingForPackage("typescript", typePathMappings);
@@ -115,20 +153,16 @@ export async function createVirtualLanguageServiceHost(
115153
));
116154

117155
// Add all types except @sapui5/types which will be handled below
118-
options.types.push(...typePackageDirs.filter((dir) => dir !== "/types/@sapui5/types/"));
156+
compilerOptions.types.push(...typePackageDirs.filter((dir) => dir !== "/types/@sapui5/types/"));
119157

120158
// Adds types / mappings for all @sapui5/types
121159
addSapui5TypesMappingToCompilerOptions(
122-
await collectSapui5TypesFiles(), options, context, libraryDependencies);
160+
await collectSapui5TypesFiles(), compilerOptions, context, libraryDependencies);
123161

124162
// Create regex matching all path mapping keys
125163
const pathMappingRegex = new RegExp(
126164
`^\\/types\\/(${Array.from(typePathMappings.keys()).join("|").replaceAll("/", "\\/")})\\/(.*)`);
127165

128-
if (!options.rootDir) {
129-
throw new Error(`Missing option 'rootDir'`);
130-
}
131-
132166
function mapToTypePath(fileName: string): string | undefined {
133167
const pkgName = fileName.match(pathMappingRegex);
134168
if (pkgName && pkgName.length === 3) {
@@ -166,13 +200,13 @@ export async function createVirtualLanguageServiceHost(
166200
}
167201

168202
if (silly) {
169-
log.silly(`compilerOptions: ${JSON.stringify(options, null, 2)}`);
203+
log.silly(`compilerOptions: ${JSON.stringify(compilerOptions, null, 2)}`);
170204
}
171205

172206
return {
173207

174208
getCompilationSettings: () => {
175-
return options;
209+
return compilerOptions;
176210
},
177211

178212
getScriptFileNames: () => {
@@ -228,7 +262,7 @@ export async function createVirtualLanguageServiceHost(
228262
if (silly) {
229263
log.silly(`getCurrentDirectory`);
230264
}
231-
return options.rootDir ?? "/";
265+
return compilerOptions.rootDir ?? "/";
232266
},
233267

234268
readFile: (fileName) => {

test/lib/linter/ui5Types/host.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import test from "ava";
2+
import {createVirtualLanguageServiceHost} from "../../../../src/linter/ui5Types/host.js";
3+
import LinterContext from "../../../../src/linter/LinterContext.js";
4+
import SharedLanguageService from "../../../../src/linter/ui5Types/SharedLanguageService.js";
5+
6+
// List of TypeScript library files that are loaded by default in the virtual language service host.
7+
const LIB_TYPE_FILES = [
8+
"/types/typescript/lib/lib.es5.d.ts",
9+
"/types/typescript/lib/lib.es2015.d.ts",
10+
"/types/typescript/lib/lib.es2016.d.ts",
11+
"/types/typescript/lib/lib.es2017.d.ts",
12+
"/types/typescript/lib/lib.es2018.d.ts",
13+
"/types/typescript/lib/lib.es2019.d.ts",
14+
"/types/typescript/lib/lib.es2020.d.ts",
15+
"/types/typescript/lib/lib.es2021.d.ts",
16+
"/types/typescript/lib/lib.es2022.d.ts",
17+
"/types/typescript/lib/lib.dom.d.ts",
18+
"/types/typescript/lib/lib.es2015.core.d.ts",
19+
"/types/typescript/lib/lib.es2015.collection.d.ts",
20+
"/types/typescript/lib/lib.es2015.generator.d.ts",
21+
"/types/typescript/lib/lib.es2015.iterable.d.ts",
22+
"/types/typescript/lib/lib.es2015.promise.d.ts",
23+
"/types/typescript/lib/lib.es2015.proxy.d.ts",
24+
"/types/typescript/lib/lib.es2015.reflect.d.ts",
25+
"/types/typescript/lib/lib.es2015.symbol.d.ts",
26+
"/types/typescript/lib/lib.es2015.symbol.wellknown.d.ts",
27+
"/types/typescript/lib/lib.es2016.array.include.d.ts",
28+
"/types/typescript/lib/lib.es2016.intl.d.ts",
29+
"/types/typescript/lib/lib.es2017.arraybuffer.d.ts",
30+
"/types/typescript/lib/lib.es2017.date.d.ts",
31+
"/types/typescript/lib/lib.es2017.object.d.ts",
32+
"/types/typescript/lib/lib.es2017.sharedmemory.d.ts",
33+
"/types/typescript/lib/lib.es2017.string.d.ts",
34+
"/types/typescript/lib/lib.es2017.intl.d.ts",
35+
"/types/typescript/lib/lib.es2017.typedarrays.d.ts",
36+
"/types/typescript/lib/lib.es2018.asyncgenerator.d.ts",
37+
"/types/typescript/lib/lib.es2018.asynciterable.d.ts",
38+
"/types/typescript/lib/lib.es2018.intl.d.ts",
39+
"/types/typescript/lib/lib.es2018.promise.d.ts",
40+
"/types/typescript/lib/lib.es2018.regexp.d.ts",
41+
"/types/typescript/lib/lib.es2019.array.d.ts",
42+
"/types/typescript/lib/lib.es2019.object.d.ts",
43+
"/types/typescript/lib/lib.es2019.string.d.ts",
44+
"/types/typescript/lib/lib.es2019.symbol.d.ts",
45+
"/types/typescript/lib/lib.es2019.intl.d.ts",
46+
"/types/typescript/lib/lib.es2020.bigint.d.ts",
47+
"/types/typescript/lib/lib.es2020.date.d.ts",
48+
"/types/typescript/lib/lib.es2020.promise.d.ts",
49+
"/types/typescript/lib/lib.es2020.sharedmemory.d.ts",
50+
"/types/typescript/lib/lib.es2020.string.d.ts",
51+
"/types/typescript/lib/lib.es2020.symbol.wellknown.d.ts",
52+
"/types/typescript/lib/lib.es2020.intl.d.ts",
53+
"/types/typescript/lib/lib.es2020.number.d.ts",
54+
"/types/typescript/lib/lib.es2021.promise.d.ts",
55+
"/types/typescript/lib/lib.es2021.string.d.ts",
56+
"/types/typescript/lib/lib.es2021.weakref.d.ts",
57+
"/types/typescript/lib/lib.es2021.intl.d.ts",
58+
"/types/typescript/lib/lib.es2022.array.d.ts",
59+
"/types/typescript/lib/lib.es2022.error.d.ts",
60+
"/types/typescript/lib/lib.es2022.intl.d.ts",
61+
"/types/typescript/lib/lib.es2022.object.d.ts",
62+
"/types/typescript/lib/lib.es2022.string.d.ts",
63+
"/types/typescript/lib/lib.es2022.regexp.d.ts",
64+
"/types/typescript/lib/lib.decorators.d.ts",
65+
"/types/typescript/lib/lib.decorators.legacy.d.ts",
66+
];
67+
68+
test("createVirtualLanguageServiceHost: Empty project", async (t) => {
69+
const sharedLanguageService = new SharedLanguageService();
70+
71+
const fileContents = new Map<string, string>([
72+
["/resources/test/test.js", ""],
73+
]);
74+
const sourceMaps = new Map<string, string>();
75+
const context = new LinterContext({
76+
rootDir: "/",
77+
namespace: "test",
78+
});
79+
const projectScriptVersion = sharedLanguageService.getNextProjectScriptVersion();
80+
81+
const host = await createVirtualLanguageServiceHost(
82+
{}, fileContents, sourceMaps, context, projectScriptVersion, undefined
83+
);
84+
85+
sharedLanguageService.acquire(host);
86+
87+
const program = sharedLanguageService.getProgram();
88+
89+
// Check for the minimum loaded files. This needs to be adjusted when the default compiler options
90+
// are changed or additional type definitions are added.
91+
const sourceFileNames = program.getSourceFiles().map((sf) => sf.fileName);
92+
t.deepEqual(sourceFileNames, [
93+
...LIB_TYPE_FILES,
94+
"/resources/test/test.js",
95+
"/types/@types/jquery/JQueryStatic.d.ts",
96+
"/types/@types/jquery/JQuery.d.ts",
97+
"/types/@types/jquery/misc.d.ts",
98+
"/types/@types/jquery/legacy.d.ts",
99+
"/types/@types/jquery/index.d.ts",
100+
"/types/@types/qunit/index.d.ts",
101+
"/types/@types/sizzle/index.d.ts",
102+
"/types/@ui5/linter/types/jquery.sap.mobile.d.ts",
103+
"/types/@ui5/linter/types/jquery.sap.d.ts",
104+
"/types/@ui5/linter/types/index.d.ts",
105+
"/types/@sapui5/types/types/sap.ui.core.d.ts",
106+
"/types/@ui5/linter/types/pseudo-modules/sap.ui.core.d.ts",
107+
"/types/@ui5/linter/types/sapui5/sap.ui.core.d.ts",
108+
]);
109+
});
110+
111+
test("createVirtualLanguageServiceHost: Minimal project with sap/m/Button import", async (t) => {
112+
const sharedLanguageService = new SharedLanguageService();
113+
114+
const fileContents = new Map<string, string>([
115+
["/resources/test/test.js", "sap.ui.define(['sap/m/Button'], function() {});"],
116+
]);
117+
const sourceMaps = new Map<string, string>();
118+
const context = new LinterContext({
119+
rootDir: "/",
120+
namespace: "test",
121+
});
122+
const projectScriptVersion = sharedLanguageService.getNextProjectScriptVersion();
123+
124+
const host = await createVirtualLanguageServiceHost(
125+
{}, fileContents, sourceMaps, context, projectScriptVersion, undefined
126+
);
127+
128+
sharedLanguageService.acquire(host);
129+
130+
const program = sharedLanguageService.getProgram();
131+
132+
// Check for the minimum loaded files. This needs to be adjusted when the default compiler options
133+
// are changed or additional type definitions are added.
134+
const sourceFileNames = program.getSourceFiles().map((sf) => sf.fileName);
135+
t.deepEqual(sourceFileNames, [
136+
...LIB_TYPE_FILES,
137+
"/types/@sapui5/types/types/sap.ui.core.d.ts",
138+
"/types/@ui5/linter/types/pseudo-modules/sap.ui.core.d.ts",
139+
"/types/@ui5/linter/types/sapui5/sap.ui.core.d.ts",
140+
"/types/@sapui5/types/types/sap.ui.unified.d.ts",
141+
"/types/@ui5/linter/types/pseudo-modules/sap.ui.unified.d.ts",
142+
"/types/@ui5/linter/types/sapui5/sap.ui.unified.d.ts",
143+
"/types/@sapui5/types/types/sap.m.d.ts",
144+
"/types/@ui5/linter/types/pseudo-modules/sap.m.d.ts",
145+
"/types/@ui5/linter/types/sapui5/sap.m.d.ts",
146+
"/resources/test/test.js",
147+
"/types/@types/jquery/JQueryStatic.d.ts",
148+
"/types/@types/jquery/JQuery.d.ts",
149+
"/types/@types/jquery/misc.d.ts",
150+
"/types/@types/jquery/legacy.d.ts",
151+
"/types/@types/jquery/index.d.ts",
152+
"/types/@types/qunit/index.d.ts",
153+
"/types/@types/sizzle/index.d.ts",
154+
"/types/@ui5/linter/types/jquery.sap.mobile.d.ts",
155+
"/types/@ui5/linter/types/jquery.sap.d.ts",
156+
"/types/@ui5/linter/types/index.d.ts",
157+
]);
158+
});

0 commit comments

Comments
 (0)