Skip to content

Commit 829988b

Browse files
ayazhafizmhevery
authored andcommitted
fix(language-service): do not treat file URIs as general URLs (#39917)
In the past, the legacy (VE-based) language service would use a `UrlResolver` instance to resolve file paths, primarily for compiler resources like external templates. The problem with this is that the UrlResolver is designed to resolve URLs in general, and so for a path like `/a/b/#c`, `#c` is treated as hash/fragment rather than as part of the path, which can lead to unexpected path resolution (f.x., `resolve('a/b/#c/d.ts', './d.html')` would produce `'a/b/d.html'` rather than the expected `'a/b/#c/d.html'`). This commit resolves the issue by using Node's `path` module to resolve file paths directly, which aligns more with how resources are resolved in the Ivy compiler. The testing story here is not great, and the API for validating a file path could be a little bit prettier/robust. However, since the VE-based language service is going into more of a "maintenance mode" now that there is a clear path for the Ivy-based LS moving forward, I think it is okay not to spend too much time here. Closes angular/vscode-ng-language-service#892 Closes angular/vscode-ng-language-service#1001 PR Close #39917
1 parent 35309bb commit 829988b

File tree

7 files changed

+50
-12
lines changed

7 files changed

+50
-12
lines changed

packages/language-service/src/typescript_host.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {analyzeNgModules, AotSummaryResolver, CompileDirectiveSummary, CompileMetadataResolver, CompileNgModuleMetadata, CompilePipeSummary, CompilerConfig, createOfflineCompileUrlResolver, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, FormattedError, FormattedMessageChain, HtmlParser, isFormattedError, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, Parser, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser} from '@angular/compiler';
9+
import {analyzeNgModules, AotSummaryResolver, CompileDirectiveSummary, CompileMetadataResolver, CompileNgModuleMetadata, CompilePipeSummary, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, FormattedError, FormattedMessageChain, HtmlParser, isFormattedError, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, Parser, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, UrlResolver} from '@angular/compiler';
1010
import {SchemaMetadata, ViewEncapsulation, ɵConsole as Console} from '@angular/core';
11+
import * as path from 'path';
1112
import * as tss from 'typescript/lib/tsserverlibrary';
1213

1314
import {createLanguageService} from './language_service';
@@ -64,6 +65,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
6465
private readonly fileToComponent = new Map<string, StaticSymbol>();
6566
private readonly collectedErrors = new Map<string, any[]>();
6667
private readonly fileVersions = new Map<string, string>();
68+
private readonly urlResolver: UrlResolver;
6769

6870
private lastProgram: tss.Program|undefined = undefined;
6971
private analyzedModules: NgAnalyzedModules = {
@@ -93,6 +95,16 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
9395
this.staticSymbolResolver = new StaticSymbolResolver(
9496
this.reflectorHost, this.staticSymbolCache, this.summaryResolver,
9597
(e, filePath) => this.collectError(e, filePath));
98+
this.urlResolver = {
99+
resolve: (baseUrl: string, url: string) => {
100+
// In practice, `directoryExists` is always defined.
101+
// https://github.com/microsoft/TypeScript/blob/0b6c9254a850dd07056259d4eefca7721745af75/src/server/project.ts#L1608-L1614
102+
if (tsLsHost.directoryExists!(baseUrl)) {
103+
return path.resolve(baseUrl, url);
104+
}
105+
return path.resolve(path.dirname(baseUrl), url);
106+
}
107+
};
96108
}
97109

98110
// The resolver is instantiated lazily and should not be accessed directly.
@@ -125,7 +137,6 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
125137
const pipeResolver = new PipeResolver(staticReflector);
126138
const elementSchemaRegistry = new DomElementSchemaRegistry();
127139
const resourceLoader = new DummyResourceLoader();
128-
const urlResolver = createOfflineCompileUrlResolver();
129140
const htmlParser = new DummyHtmlParser();
130141
// This tracks the CompileConfig in codegen.ts. Currently these options
131142
// are hard-coded.
@@ -134,7 +145,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
134145
useJit: false,
135146
});
136147
const directiveNormalizer =
137-
new DirectiveNormalizer(resourceLoader, urlResolver, htmlParser, config);
148+
new DirectiveNormalizer(resourceLoader, this.urlResolver, htmlParser, config);
138149
this._resolver = new CompileMetadataResolver(
139150
config, htmlParser, moduleResolver, directiveResolver, pipeResolver,
140151
new JitSummaryResolver(), elementSchemaRegistry, directiveNormalizer, new Console(),
@@ -192,12 +203,11 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
192203
}
193204

194205
// update template references and fileToComponent
195-
const urlResolver = createOfflineCompileUrlResolver();
196206
for (const ngModule of this.analyzedModules.ngModules) {
197207
for (const directive of ngModule.declaredDirectives) {
198208
const {metadata} = this.resolver.getNonNormalizedDirectiveMetadata(directive.reference)!;
199209
if (metadata.isComponent && metadata.template && metadata.template.templateUrl) {
200-
const templateName = urlResolver.resolve(
210+
const templateName = this.urlResolver.resolve(
201211
this.reflector.componentModuleUrl(directive.reference),
202212
metadata.template.templateUrl);
203213
this.fileToComponent.set(templateName, directive.reference);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Component} from '@angular/core';
10+
11+
@Component({
12+
selector: 'inner',
13+
templateUrl: './inner.html',
14+
})
15+
export class InnerComponent {
16+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div>Hello</div>

packages/language-service/test/project/app/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
import {CommonModule} from '@angular/common';
1010
import {NgModule} from '@angular/core';
1111
import {FormsModule} from '@angular/forms';
12+
import {InnerComponent} from './#inner/component';
1213
import {AppComponent} from './app.component';
1314
import * as ParsingCases from './parsing-cases';
1415

1516
@NgModule({
1617
imports: [CommonModule, FormsModule],
1718
declarations: [
1819
AppComponent,
20+
InnerComponent,
1921
ParsingCases.CounterDirective,
2022
ParsingCases.HintModel,
2123
ParsingCases.NumberModel,

packages/language-service/test/test_utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function loadTourOfHeroes(): ReadonlyMap<string, string> {
6969
const value = fs.readFileSync(absPath, 'utf8');
7070
files.set(key, value);
7171
} else {
72-
const key = path.join('/', filePath);
72+
const key = path.join('/', path.relative(root, absPath));
7373
files.set(key, '[[directory]]');
7474
dirs.push(absPath);
7575
}
@@ -189,7 +189,7 @@ export class MockTypescriptHost implements ts.LanguageServiceHost {
189189
if (this.overrideDirectory.has(directoryName)) return true;
190190
const effectiveName = this.getEffectiveName(directoryName);
191191
if (effectiveName === directoryName) {
192-
return TOH.has(directoryName);
192+
return TOH.get(directoryName) === '[[directory]]';
193193
}
194194
if (effectiveName === '/' + this.node_modules) {
195195
return true;

packages/language-service/test/ts_plugin_spec.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ describe('plugin', () => {
5858
const compilerDiags = tsLS.getCompilerOptionsDiagnostics();
5959
expect(compilerDiags).toEqual([]);
6060
const sourceFiles = program.getSourceFiles().filter(f => !f.fileName.endsWith('.d.ts'));
61-
// there are three .ts files in the test project
62-
expect(sourceFiles.length).toBe(3);
61+
// there are four .ts files in the test project
62+
expect(sourceFiles.length).toBe(4);
6363
for (const {fileName} of sourceFiles) {
6464
const syntacticDiags = tsLS.getSyntacticDiagnostics(fileName);
6565
expect(syntacticDiags).toEqual([]);
@@ -133,9 +133,10 @@ describe('plugin', () => {
133133

134134
it('should return external templates when getExternalFiles() is called', () => {
135135
const externalTemplates = getExternalFiles(mockProject);
136-
expect(externalTemplates).toEqual([
136+
expect(new Set(externalTemplates)).toEqual(new Set([
137137
'/app/test.ng',
138-
]);
138+
'/app/#inner/inner.html',
139+
]));
139140
});
140141

141142
it('should not return external template that does not exist', () => {

packages/language-service/test/typescript_host_spec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import * as ngc from '@angular/compiler';
109
import * as ts from 'typescript';
1110

1211
import {TypeScriptServiceHost} from '../src/typescript_host';
@@ -109,6 +108,15 @@ describe('TypeScriptServiceHost', () => {
109108
expect(template.source).toContain('<h2>{{hero.name}} details!</h2>');
110109
});
111110

111+
// https://github.com/angular/vscode-ng-language-service/issues/892
112+
it('should resolve external templates with `#` in the path', () => {
113+
const tsLSHost = new MockTypescriptHost(['/app/main.ts']);
114+
const tsLS = ts.createLanguageService(tsLSHost);
115+
const ngLSHost = new TypeScriptServiceHost(tsLSHost, tsLS);
116+
ngLSHost.getAnalyzedModules();
117+
expect(ngLSHost.getExternalTemplates()).toContain('/app/#inner/inner.html');
118+
});
119+
112120
// https://github.com/angular/angular/issues/32301
113121
it('should clear caches when program changes', () => {
114122
const tsLSHost = new MockTypescriptHost(['/app/main.ts']);

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy