UNPKG

17.6 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.LanguageServiceInterceptor = exports.version = void 0;
4const ts = require("typescript");
5const ymir_1 = require("@fimbul/ymir");
6const inversify_1 = require("inversify");
7const core_module_1 = require("../src/di/core.module");
8const default_module_1 = require("../src/di/default.module");
9const configuration_manager_1 = require("../src/services/configuration-manager");
10const linter_1 = require("../src/linter");
11const utils_1 = require("../src/utils");
12const cached_file_system_1 = require("../src/services/cached-file-system");
13const resolve = require("resolve");
14const path = require("path");
15const yaml = require("js-yaml");
16const argparse_1 = require("../src/argparse");
17const normalize_glob_1 = require("normalize-glob");
18const minimatch_1 = require("minimatch");
19const program_state_1 = require("../src/services/program-state");
20const config_hash_1 = require("../src/config-hash");
21const tsutils_1 = require("tsutils");
22const fix_1 = require("../src/fix");
23exports.version = '2';
24const DIAGNOSTIC_CODE = 3;
25class LanguageServiceInterceptor {
26 constructor(config,
27 // tslint:disable:no-submodule-imports
28 project, serverHost,
29 // tslint:enable:no-submodule-imports
30 languageService, require, log) {
31 this.config = config;
32 this.project = project;
33 this.serverHost = serverHost;
34 this.languageService = languageService;
35 this.require = require;
36 this.log = log;
37 this.lastProjectVersion = '';
38 this.findingsForFile = new WeakMap();
39 this.oldState = undefined;
40 }
41 updateConfig(config) {
42 this.config = config;
43 }
44 getSemanticDiagnostics(fileName) {
45 const diagnostics = this.languageService.getSemanticDiagnostics(fileName);
46 this.log(`getSemanticDiagnostics for ${fileName}`);
47 const result = this.getFindingsForFile(fileName);
48 if (!(result === null || result === void 0 ? void 0 : result.findings.length))
49 return diagnostics;
50 const findingDiagnostics = utils_1.mapDefined(result.findings, (finding) => finding.severity === 'suggestion'
51 ? undefined
52 : {
53 file: result.file,
54 category: this.config.displayErrorsAsWarnings || finding.severity === 'warning'
55 ? ts.DiagnosticCategory.Warning
56 : ts.DiagnosticCategory.Error,
57 code: DIAGNOSTIC_CODE,
58 source: 'wotan',
59 messageText: `[${finding.ruleName}] ${finding.message}`,
60 start: finding.start.position,
61 length: finding.end.position - finding.start.position,
62 });
63 return [...diagnostics, ...findingDiagnostics];
64 }
65 getSuggestionDiagnostics(fileName) {
66 const diagnostics = this.languageService.getSuggestionDiagnostics(fileName);
67 this.log(`getSuggestionDiagnostics for ${fileName}`);
68 const result = this.getFindingsForFile(fileName);
69 if (!(result === null || result === void 0 ? void 0 : result.findings.length))
70 return diagnostics;
71 const findingDiagnostics = utils_1.mapDefined(result.findings, (finding) => finding.severity !== 'suggestion'
72 ? undefined
73 : {
74 file: result.file,
75 category: ts.DiagnosticCategory.Suggestion,
76 code: DIAGNOSTIC_CODE,
77 source: 'wotan',
78 messageText: `[${finding.ruleName}] ${finding.message}`,
79 start: finding.start.position,
80 length: finding.end.position - finding.start.position,
81 });
82 return [...diagnostics, ...findingDiagnostics];
83 }
84 getCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences) {
85 const fixes = this.languageService.getCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences);
86 if (!errorCodes.includes(DIAGNOSTIC_CODE))
87 return fixes;
88 this.log(`getCodeFixesAtPosition for ${fileName} from ${start} to ${end}`);
89 const result = this.getFindingsForFile(fileName);
90 if (!result)
91 return fixes;
92 const ruleFixes = [];
93 const disables = [];
94 let fixableFindings;
95 for (const finding of result.findings) {
96 if (finding.start.position === start && finding.end.position === end) {
97 if (finding.fix !== undefined) {
98 fixableFindings !== null && fixableFindings !== void 0 ? fixableFindings : (fixableFindings = result.findings.filter((f) => f.fix !== undefined));
99 const multipleFixableFindingsForRule = fixableFindings.some((f) => f.ruleName === finding.ruleName && f !== finding);
100 ruleFixes.push({
101 fixName: 'wotan:' + finding.ruleName,
102 description: 'Fix ' + finding.ruleName,
103 fixId: multipleFixableFindingsForRule ? 'wotan:' + finding.ruleName : undefined,
104 fixAllDescription: multipleFixableFindingsForRule ? 'Fix all ' + finding.ruleName : undefined,
105 changes: [{
106 fileName,
107 textChanges: finding.fix.replacements.map((r) => ({ span: { start: r.start, length: r.end - r.start }, newText: r.text })),
108 }],
109 });
110 }
111 disables.push({
112 fixName: 'disable ' + finding.ruleName,
113 description: `Disable ${finding.ruleName} for this line`,
114 changes: [{
115 fileName,
116 textChanges: [
117 getDisableCommentChange(finding.start.position, result.file, finding.ruleName, formatOptions.newLineCharacter),
118 ],
119 }],
120 });
121 }
122 }
123 if (fixableFindings !== undefined && fixableFindings.length > 1) {
124 try {
125 const fixAll = fix_1.applyFixes(result.file.text, fixableFindings.map((f) => f.fix));
126 if (fixAll.fixed > 1)
127 ruleFixes.push({
128 fixName: 'wotan:fixall',
129 description: 'Apply all auto-fixes',
130 changes: [{
131 fileName,
132 textChanges: [{
133 span: fixAll.range.span,
134 newText: fixAll.result.substr(fixAll.range.span.start, fixAll.range.newLength),
135 }],
136 }],
137 });
138 }
139 catch (e) {
140 this.log('Error in fixAll: ' + (e === null || e === void 0 ? void 0 : e.message));
141 }
142 }
143 return [...fixes, ...ruleFixes, ...disables];
144 }
145 getCombinedCodeFix(scope, fixId, formatOptions, preferences) {
146 if (typeof fixId !== 'string' || !fixId.startsWith('wotan:'))
147 return this.languageService.getCombinedCodeFix(scope, fixId, formatOptions, preferences);
148 const findingsForFile = this.getFindingsForFile(scope.fileName);
149 const ruleName = fixId.substring('wotan:'.length);
150 const fixAll = fix_1.applyFixes(findingsForFile.file.text, utils_1.mapDefined(findingsForFile.findings, (f) => f.ruleName === ruleName ? f.fix : undefined));
151 return {
152 changes: [{
153 fileName: scope.fileName,
154 textChanges: [{
155 span: fixAll.range.span,
156 newText: fixAll.result.substr(fixAll.range.span.start, fixAll.range.newLength),
157 }],
158 }],
159 };
160 }
161 getFindingsForFile(fileName) {
162 const program = this.languageService.getProgram();
163 if (program === undefined)
164 return;
165 const file = program.getSourceFile(fileName);
166 if (file === undefined) {
167 this.log(`File ${fileName} is not included in the Program`);
168 return;
169 }
170 const projectVersion = this.project.getProjectVersion();
171 if (this.lastProjectVersion === projectVersion) {
172 const cached = this.findingsForFile.get(file);
173 if (cached !== undefined) {
174 this.log(`Reusing last result with ${cached.length} findings`);
175 return { file, findings: cached };
176 }
177 }
178 else {
179 this.findingsForFile = new WeakMap();
180 this.lastProjectVersion = projectVersion;
181 }
182 try {
183 const findings = this.getFindingsForFileWorker(file, program);
184 this.findingsForFile.set(file, findings);
185 return { file, findings };
186 }
187 catch (e) {
188 this.log(`Error linting ${fileName}: ${e === null || e === void 0 ? void 0 : e.message}`);
189 this.findingsForFile.set(file, utils_1.emptyArray);
190 return;
191 }
192 }
193 getFindingsForFileWorker(file, program) {
194 let globalConfigDir = this.project.getCurrentDirectory();
195 let globalOptions;
196 while (true) {
197 const scriptSnapshot = this.project.getScriptSnapshot(globalConfigDir + '/.fimbullinter.yaml');
198 if (scriptSnapshot !== undefined) {
199 this.log(`Using '${globalConfigDir}/.fimbullinter.yaml' for global options`);
200 globalOptions = yaml.load(scriptSnapshot.getText(0, scriptSnapshot.getLength())) || {};
201 break;
202 }
203 const parentDir = path.dirname(globalConfigDir);
204 if (parentDir === globalConfigDir) {
205 this.log("Cannot find '.fimbullinter.yaml'");
206 globalOptions = {};
207 break;
208 }
209 globalConfigDir = parentDir;
210 }
211 const globalConfig = argparse_1.parseGlobalOptions(globalOptions);
212 if (!isIncluded(file.fileName, globalConfigDir, globalConfig)) {
213 this.log('File is excluded by global options');
214 return [];
215 }
216 const container = new inversify_1.Container({ defaultScope: inversify_1.BindingScopeEnum.Singleton });
217 for (const module of globalConfig.modules)
218 container.load(this.loadPluginModule(module, globalConfigDir, globalOptions));
219 container.bind(ymir_1.StatePersistence).toConstantValue({
220 loadState: () => this.oldState,
221 saveState: (_, state) => this.oldState = state,
222 });
223 container.bind(ymir_1.ContentId).toConstantValue({
224 forFile: (fileName) => this.project.getScriptVersion(fileName),
225 });
226 container.bind(ymir_1.FileSystem).toConstantValue(new ProjectFileSystem(this.project));
227 container.bind(ymir_1.DirectoryService).toConstantValue({
228 getCurrentDirectory: () => this.project.getCurrentDirectory(),
229 });
230 container.bind(ymir_1.Resolver).toDynamicValue((context) => {
231 const fs = context.container.get(cached_file_system_1.CachedFileSystem);
232 return {
233 getDefaultExtensions() {
234 return ['.js'];
235 },
236 resolve(id, basedir, extensions = ['.js'], paths) {
237 return resolve.sync(id, {
238 basedir,
239 extensions,
240 paths,
241 isFile: (f) => fs.isFile(f),
242 readFileSync: (f) => fs.readFile(f),
243 });
244 },
245 require: this.require,
246 };
247 });
248 const warnings = [];
249 container.bind(ymir_1.MessageHandler).toConstantValue({
250 log: this.log,
251 warn: (message) => {
252 if (utils_1.addUnique(warnings, message))
253 this.log(message);
254 },
255 error(e) {
256 this.log(e.message);
257 },
258 });
259 container.load(core_module_1.createCoreModule(globalOptions), default_module_1.createDefaultModule());
260 const fileFilter = container.get(ymir_1.FileFilterFactory).create({ program, host: this.project });
261 if (!fileFilter.filter(file)) {
262 this.log('File is excluded by FileFilter');
263 return [];
264 }
265 const configManager = container.get(configuration_manager_1.ConfigurationManager);
266 const config = globalConfig.config === undefined
267 ? configManager.find(file.fileName)
268 : configManager.loadLocalOrResolved(globalConfig.config, globalConfigDir);
269 const effectiveConfig = config && configManager.reduce(config, file.fileName);
270 if (effectiveConfig === undefined) {
271 this.log('File is excluded by configuration');
272 return [];
273 }
274 const linterOptions = {
275 reportUselessDirectives: globalConfig.reportUselessDirectives
276 ? globalConfig.reportUselessDirectives === true
277 ? 'error'
278 : globalConfig.reportUselessDirectives
279 : undefined,
280 };
281 const programState = container.get(program_state_1.ProgramStateFactory).create(program, this.project, this.project.projectName);
282 const configHash = config_hash_1.createConfigHash(effectiveConfig, linterOptions);
283 const cached = programState.getUpToDateResult(file.fileName, configHash);
284 if (cached !== undefined) {
285 this.log(`Using ${cached.length} cached findings`);
286 return cached;
287 }
288 this.log('Start linting');
289 const linter = container.get(linter_1.Linter);
290 const result = linter.lintFile(file, effectiveConfig, program, linterOptions);
291 programState.setFileResult(file.fileName, configHash, result);
292 programState.save();
293 this.log(`Found ${result.length} findings`);
294 return result;
295 }
296 loadPluginModule(moduleName, basedir, options) {
297 moduleName = resolve.sync(moduleName, {
298 basedir,
299 extensions: ['.js'],
300 isFile: (f) => this.project.fileExists(f),
301 readFileSync: (f) => this.project.readFile(f),
302 });
303 const m = this.require(moduleName);
304 if (!m || typeof m.createModule !== 'function')
305 throw new Error(`Module '${moduleName}' does not export a function 'createModule'`);
306 return m.createModule(options);
307 }
308 getSupportedCodeFixes(fixes) {
309 return [...fixes, '' + DIAGNOSTIC_CODE];
310 }
311 cleanupSemanticCache() {
312 this.findingsForFile = new WeakMap();
313 this.oldState = undefined;
314 }
315 dispose() {
316 return this.languageService.dispose();
317 }
318}
319exports.LanguageServiceInterceptor = LanguageServiceInterceptor;
320function isIncluded(fileName, basedir, options) {
321 outer: if (options.files.length !== 0) {
322 for (const include of options.files)
323 for (const normalized of normalize_glob_1.normalizeGlob(include, basedir))
324 if (new minimatch_1.Minimatch(normalized).match(fileName))
325 break outer;
326 return false;
327 }
328 for (const exclude of options.exclude)
329 for (const normalized of normalize_glob_1.normalizeGlob(exclude, basedir))
330 if (new minimatch_1.Minimatch(normalized, { dot: true }).match(fileName))
331 return false;
332 return true;
333}
334class ProjectFileSystem {
335 constructor(host) {
336 this.host = host;
337 this.realpath = this.host.realpath && ((f) => this.host.realpath(f));
338 }
339 createDirectory() {
340 throw new Error('should not be called');
341 }
342 deleteFile() {
343 throw new Error('should not be called');
344 }
345 writeFile() {
346 throw new Error('should not be called');
347 }
348 normalizePath(f) {
349 f = f.replace(/\\/g, '/');
350 return this.host.useCaseSensitiveFileNames() ? f : f.toLowerCase();
351 }
352 readFile(f) {
353 const result = this.host.readFile(f);
354 if (result === undefined)
355 throw new Error('ENOENT');
356 return result;
357 }
358 readDirectory(dir) {
359 return this.host.readDirectory(dir, undefined, undefined, ['*']);
360 }
361 stat(f) {
362 const isFile = this.host.fileExists(f)
363 ? true
364 : this.host.directoryExists(f)
365 ? false
366 : undefined;
367 return {
368 isDirectory() {
369 return isFile === false;
370 },
371 isFile() {
372 return isFile === true;
373 },
374 };
375 }
376}
377// TODO this should be done by Linter or FindingFilter
378function getDisableCommentChange(pos, sourceFile, ruleName, newline = tsutils_1.getLineBreakStyle(sourceFile)) {
379 const lineStart = pos - ts.getLineAndCharacterOfPosition(sourceFile, pos).character;
380 let whitespace = '';
381 for (let i = lineStart, ch; i < sourceFile.text.length; i += charSize(ch)) {
382 ch = sourceFile.text.codePointAt(i);
383 if (ts.isWhiteSpaceSingleLine(ch)) {
384 whitespace += String.fromCodePoint(ch);
385 }
386 else {
387 break;
388 }
389 }
390 return {
391 newText: `${whitespace}// wotan-disable-next-line ${ruleName}${newline}`,
392 span: {
393 start: lineStart,
394 length: 0,
395 },
396 };
397}
398function charSize(ch) {
399 return ch >= 0x10000 ? 2 : 1;
400}
401//# sourceMappingURL=index.js.map
\No newline at end of file