-
Notifications
You must be signed in to change notification settings - Fork 29.4k
feat(language-service): [Ivy] getSemanticDiagnostics for external templates #39065
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example, could we clear the modifiedTemplates here and return the previous contents? (If the compiler only needs the same changes once)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's not a good idea to clear the modified templates here, because
This assumption is fragile. Within the compiler it's perfectly fine for it to call this method multiple times, and it'd be surprising to them if the results of the first call is different from the second call
This is a method defined by the compiler, and we need to adhere to the behavior they expect, not the behavior we provide.
angular/packages/compiler-cli/src/ngtsc/core/api/src/interfaces.ts
Lines 47 to 51 in bc69182
I gave this some more thought, and realize that we could clear the modified template after the compiler has read the updated version. The pro is that it'll always be consistent, i.e. once the compiler reads the template, it's no longer marked as modified. The con is that there's no explicit way for language service to make sure the template update is cleared after every request. I think pro > con in this case, because it's very unlikely that the compiler would not read a template that's been marked as modified.
With this, I deleted the
clearTemplateUpdates()method.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more note: let's say the compiler asks for all the modified resources (getModifiedResourceFiles), then reads one modified resource, then asks for the modified resources again. In this case, with what you have described, the second set of modified resources would be one less than the first, so the expectation that first call = second call in the same run is lost. Is this something we need to be concerned about?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the behavior is still consistent in this case, because once the compiler reads a template, it already has the latest state of the file, so the file is no longer considered "modified".
That said, I agree this is not the best design. I think the initial design with
clearTemplateUpdates()is the most straightforward solution with no surprise. Would you prefer that over the current solution?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't we just recreate the program adapter each time we create a compiler?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The adapter is stateful because it needs to keep track of the template versions.
We could make it stateless, but in that case, the language service class itself will have to keep track of the template versions. I think the responsibility of tracking versions should fall under the adapter, not the language service.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh youre right I forgot about the versioning. Okay I am fine either way, sorry this was such a long discussion. Maybe later on we can figure out some nicer abstractions, even on the compiler side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's good, your comments make me think more about the architecture, and I agree there's an opportunity here to reuse some of TS logic to keep track of changed files, instead of us doing our custom checking.
Could you please approve this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course :)