Skip to content
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

Ivy LS: Emit diagnostics for parse errors #39434

Open
ayazhafiz opened this issue Oct 26, 2020 · 2 comments
Open

Ivy LS: Emit diagnostics for parse errors #39434

ayazhafiz opened this issue Oct 26, 2020 · 2 comments

Comments

@ayazhafiz
Copy link
Contributor

@ayazhafiz ayazhafiz commented Oct 26, 2020

Currently when queried for semantic diagnostics, the language service only returns diagnostics from typechecking the program TCB. However parse errors are likely to be just as (if not more) frequent than type errors, so we should return those as well.

When getSemanticDiagnostics is called, we retrieve a compiler for the program that file is in. The file is checked for changed contents; if contents have been changed and it's an external template, we update known components using the template to have the new template:

private overrideTemplate(fileName: string, compiler: NgCompiler) {
if (!this.adapter.isTemplateDirty(fileName)) {
return;
}
// 1. Get the latest snapshot
const latestTemplate = this.adapter.readResource(fileName);
// 2. Find all components that use the template
const ttc = compiler.getTemplateTypeChecker();
const components = compiler.getComponentsWithTemplateFile(fileName);
// 3. Update component template
for (const component of components) {
if (ts.isClassDeclaration(component)) {
ttc.overrideComponentTemplate(component, latestTemplate);
}
}
}

ttc.overrideComponentTemplate parses the template and returns errors, so our work is nearly done. The only thing left to do is marshall the each of the compiler's ParseError into a ts.Diagnostic. The compiler also provides a method for creating ts.Diagnostics for a template:

export function makeTemplateDiagnostic(
templateId: TemplateId, mapping: TemplateSourceMapping, span: ParseSourceSpan,
category: ts.DiagnosticCategory, code: number, messageText: string|ts.DiagnosticMessageChain,
relatedMessage?: {
text: string,
span: ParseSourceSpan,
}): TemplateDiagnostic {

A usage of which for ParseErrors is already present for the collection of components during the regular compilation pipeline:

if (template.errors !== null) {
// If there are any template parsing errors, convert them to `ts.Diagnostic`s for display.
const id = getTemplateId(node);
diagnostics = template.errors.map(error => {
const span = error.span;
if (span.start.offset === span.end.offset) {
// Template errors can contain zero-length spans, if the error occurs at a single point.
// However, TypeScript does not handle displaying a zero-length diagnostic very well, so
// increase the ending offset by 1 for such errors, to ensure the position is shown in the
// diagnostic.
span.end.offset++;
}
return makeTemplateDiagnostic(
id, template.sourceMapping, span, ts.DiagnosticCategory.Error,
ngErrorCode(ErrorCode.TEMPLATE_PARSE_ERROR), error.msg);
});
}

Most of the "hard" work is already done, the only missing piece is the mapping: TemplateSourceMapping parameter expected by makeTemplateDiagnostic. Actually we have this information too, we just need TemplateTypeChecker#overrideComponentTemplate to return it to us:

overrideComponentTemplate(component: ts.ClassDeclaration, template: string):
{nodes: TmplAstNode[], errors?: ParseError[]} {
const {nodes, errors} = parseTemplate(template, 'override.html', {
preserveWhitespaces: true,
leadingTriviaChars: [],
});
if (errors !== null) {
return {nodes, errors};
}
const filePath = absoluteFromSourceFile(component.getSourceFile());
const fileRecord = this.getFileData(filePath);
const id = fileRecord.sourceManager.getTemplateId(component);
if (fileRecord.templateOverrides === null) {
fileRecord.templateOverrides = new Map();
}
fileRecord.templateOverrides.set(id, nodes);
// Clear data for the shim in question, so it'll be regenerated on the next request.
const shimFile = this.typeCheckingStrategy.shimPathForComponent(component);
fileRecord.shimData.delete(shimFile);
fileRecord.isComplete = false;
this.isComplete = false;
return {nodes};
}

fileRecord.sourceManager has information about the TemplateSourceMapping for the particular template, so we can get it from there. In particular note that overrideComponentTemplate, when called by the language service (AFAIK also the only user) should only call this for external templates (we may need to make this contract more strict/explicit), so there shouldn't need to be any extra work to be done here.

This is a "good first issue"; I have tagged it as such, if no one wants to claim it I will take a stab when I have some free time.

Side notes:

  • There may be a question of who should report these errors. TemplateTypeChecker also exposes getDiagnosticsForFile, should that be extended to include parse errors? Maybe, because then the TTC can cache this stuff and be the "source of truth" for a template, but I argue no, because it should be concerned with TypeChecker and That's It ™️
  • The language service's getOrCreateWithChangedFile now does more than just create a compiler; it also returns parse errors. Here is where I think it may make sense to make the contract between what language service queries should be used to identify changes in the workspace, and which can called without changed, more strict. In particular, I suggest one of the following:
    • Expose a didChange API that notifies the LS of workspace changes and returns semantic diagnostics. This fits more cleanly into how the LSP actually works (getSemanticDiagnostics is not exposed by the LSP, a server publishes a message containing diagnostics to the client). The problem with this is ts.LanguageService does not have such a method, making this idea incompatible if we need to keep the Angular LS as a tsserver plugin (but even in that case it might work if we just assume getSemanticDiagnostic calls on tsserver == changed workspace)
    • Make a strict contract that when a workspace is changed, getSemanticDiagnostics is always called and answered before any other LS API is called.
    • Propagate parse errors to the type checking phase (a sort-of "advanced" case that requires a lot of additional work, could get confusing quickly, and is probably not worth it for now)

Kind-of related to #38061

@ayazhafiz ayazhafiz added this to Backlog in Ivy Language Service via automation Oct 26, 2020
@ngbot ngbot bot modified the milestone: needsTriage Oct 26, 2020
@ayazhafiz ayazhafiz modified the milestones: needsTriage, Backlog Oct 26, 2020
@ngbot ngbot bot modified the milestones: Backlog, needsTriage Oct 26, 2020
@ayazhafiz ayazhafiz modified the milestones: needsTriage, Backlog Oct 27, 2020
@ngbot ngbot bot modified the milestones: Backlog, needsTriage Oct 27, 2020
@ngbot ngbot bot modified the milestones: needsTriage, Backlog Oct 27, 2020
@ayazhafiz
Copy link
Contributor Author

@ayazhafiz ayazhafiz commented Oct 27, 2020

Discussed in meeting: we should keep the parse errors in the state of the compiler, such that getSemanticDiagnostics always returns the parse errors of the current template (type errors are already cached correctly)

@ayazhafiz
Copy link
Contributor Author

@ayazhafiz ayazhafiz commented Oct 29, 2020

removing good first issue b/c not so easy to keep track of parse errors on compiler state

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
2 participants
You can’t perform that action at this time.