-
Notifications
You must be signed in to change notification settings - Fork 25.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(language-service): [Ivy] getSemanticDiagnostics for external tem…
…plates (#39065) This PR enables `getSemanticDiagnostics()` to be called on external templates. Several changes are needed to land this feature: 1. The adapter needs to implement two additional methods: a. `readResource()` Load the template from snapshot instead of reading from disk b. `getModifiedResourceFiles()` Inform the compiler that external templates have changed so that the loader could invalidate its internal cache. 2. Create `ScriptInfo` for external templates in MockHost. Prior to this, MockHost only track changes in TypeScript files. Now it needs to create `ScriptInfo` for external templates as well. For (1), in order to make sure we don't reload the template if it hasn't changed, we need to keep track of its version. Since the complexity has increased, the adapter is refactored into its own class. PR Close #39065
- Loading branch information
Showing
5 changed files
with
241 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
111 changes: 111 additions & 0 deletions
111
packages/language-service/ivy/language_service_adapter.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {NgCompilerAdapter} from '@angular/compiler-cli/src/ngtsc/core/api'; | ||
import {absoluteFrom, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system'; | ||
import {isShim} from '@angular/compiler-cli/src/ngtsc/shims'; | ||
import * as ts from 'typescript/lib/tsserverlibrary'; | ||
|
||
export class LanguageServiceAdapter implements NgCompilerAdapter { | ||
readonly entryPoint = null; | ||
readonly constructionDiagnostics: ts.Diagnostic[] = []; | ||
readonly ignoreForEmit: Set<ts.SourceFile> = new Set(); | ||
readonly factoryTracker = null; // no .ngfactory shims | ||
readonly unifiedModulesHost = null; // only used in Bazel | ||
readonly rootDirs: AbsoluteFsPath[]; | ||
private readonly templateVersion = new Map<string, string>(); | ||
private readonly modifiedTemplates = new Set<string>(); | ||
|
||
constructor(private readonly project: ts.server.Project) { | ||
this.rootDirs = project.getCompilationSettings().rootDirs?.map(absoluteFrom) || []; | ||
} | ||
|
||
isShim(sf: ts.SourceFile): boolean { | ||
return isShim(sf); | ||
} | ||
|
||
fileExists(fileName: string): boolean { | ||
return this.project.fileExists(fileName); | ||
} | ||
|
||
readFile(fileName: string): string|undefined { | ||
return this.project.readFile(fileName); | ||
} | ||
|
||
getCurrentDirectory(): string { | ||
return this.project.getCurrentDirectory(); | ||
} | ||
|
||
getCanonicalFileName(fileName: string): string { | ||
return this.project.projectService.toCanonicalFileName(fileName); | ||
} | ||
|
||
/** | ||
* readResource() is an Angular-specific method for reading files that are not | ||
* managed by the TS compiler host, namely templates and stylesheets. | ||
* It is a method on ExtendedTsCompilerHost, see | ||
* packages/compiler-cli/src/ngtsc/core/api/src/interfaces.ts | ||
*/ | ||
readResource(fileName: string): string { | ||
if (isTypeScriptFile(fileName)) { | ||
throw new Error(`readResource() should not be called on TS file: ${fileName}`); | ||
} | ||
// Calling getScriptSnapshot() will actually create a ScriptInfo if it does | ||
// not exist! The same applies for getScriptVersion(). | ||
// getScriptInfo() will not create one if it does not exist. | ||
// In this case, we *want* a script info to be created so that we could | ||
// keep track of its version. | ||
const snapshot = this.project.getScriptSnapshot(fileName); | ||
if (!snapshot) { | ||
// This would fail if the file does not exist, or readFile() fails for | ||
// whatever reasons. | ||
throw new Error(`Failed to get script snapshot while trying to read ${fileName}`); | ||
} | ||
const version = this.project.getScriptVersion(fileName); | ||
this.templateVersion.set(fileName, version); | ||
this.modifiedTemplates.delete(fileName); | ||
return snapshot.getText(0, snapshot.getLength()); | ||
} | ||
|
||
/** | ||
* getModifiedResourceFiles() is an Angular-specific method for notifying | ||
* the Angular compiler templates that have changed since it last read them. | ||
* It is a method on ExtendedTsCompilerHost, see | ||
* packages/compiler-cli/src/ngtsc/core/api/src/interfaces.ts | ||
*/ | ||
getModifiedResourceFiles(): Set<string> { | ||
return this.modifiedTemplates; | ||
} | ||
|
||
/** | ||
* Check whether the specified `fileName` is newer than the last time it was | ||
* read. If it is newer, register it and return true, otherwise do nothing and | ||
* return false. | ||
* @param fileName path to external template | ||
*/ | ||
registerTemplateUpdate(fileName: string): boolean { | ||
if (!isExternalTemplate(fileName)) { | ||
return false; | ||
} | ||
const lastVersion = this.templateVersion.get(fileName); | ||
const latestVersion = this.project.getScriptVersion(fileName); | ||
if (lastVersion !== latestVersion) { | ||
this.modifiedTemplates.add(fileName); | ||
return true; | ||
} | ||
return false; | ||
} | ||
} | ||
|
||
export function isTypeScriptFile(fileName: string): boolean { | ||
return fileName.endsWith('.ts'); | ||
} | ||
|
||
export function isExternalTemplate(fileName: string): boolean { | ||
return !isTypeScriptFile(fileName); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
49 changes: 49 additions & 0 deletions
49
packages/language-service/ivy/test/language_service_adapter_spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {LanguageServiceAdapter} from '../language_service_adapter'; | ||
import {setup, TEST_TEMPLATE} from './mock_host'; | ||
|
||
const {project, service} = setup(); | ||
|
||
describe('Language service adapter', () => { | ||
it('should register update if it has not seen the template before', () => { | ||
const adapter = new LanguageServiceAdapter(project); | ||
// Note that readResource() has never been called, so the adapter has no | ||
// knowledge of the template at all. | ||
const isRegistered = adapter.registerTemplateUpdate(TEST_TEMPLATE); | ||
expect(isRegistered).toBeTrue(); | ||
expect(adapter.getModifiedResourceFiles().size).toBe(1); | ||
}); | ||
|
||
it('should not register update if template has not changed', () => { | ||
const adapter = new LanguageServiceAdapter(project); | ||
adapter.readResource(TEST_TEMPLATE); | ||
const isRegistered = adapter.registerTemplateUpdate(TEST_TEMPLATE); | ||
expect(isRegistered).toBeFalse(); | ||
expect(adapter.getModifiedResourceFiles().size).toBe(0); | ||
}); | ||
|
||
it('should register update if template has changed', () => { | ||
const adapter = new LanguageServiceAdapter(project); | ||
adapter.readResource(TEST_TEMPLATE); | ||
service.overwrite(TEST_TEMPLATE, '<p>Hello World</p>'); | ||
const isRegistered = adapter.registerTemplateUpdate(TEST_TEMPLATE); | ||
expect(isRegistered).toBe(true); | ||
expect(adapter.getModifiedResourceFiles().size).toBe(1); | ||
}); | ||
|
||
it('should clear template updates on read', () => { | ||
const adapter = new LanguageServiceAdapter(project); | ||
const isRegistered = adapter.registerTemplateUpdate(TEST_TEMPLATE); | ||
expect(isRegistered).toBeTrue(); | ||
expect(adapter.getModifiedResourceFiles().size).toBe(1); | ||
adapter.readResource(TEST_TEMPLATE); | ||
expect(adapter.getModifiedResourceFiles().size).toBe(0); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters