refactor: move angular source to /packages rather than modules/@angular

This commit is contained in:
Jason Aden
2017-03-02 10:48:42 -08:00
parent 5ad5301a3e
commit 3e51a19983
1051 changed files with 18 additions and 18 deletions

View File

@ -0,0 +1,29 @@
/**
* @license
* Copyright Google Inc. 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
*/
export class AstPath<T> {
constructor(private path: T[]) {}
get empty(): boolean { return !this.path || !this.path.length; }
get head(): T|undefined { return this.path[0]; }
get tail(): T|undefined { return this.path[this.path.length - 1]; }
parentOf(node: T): T|undefined { return this.path[this.path.indexOf(node) - 1]; }
childOf(node: T): T|undefined { return this.path[this.path.indexOf(node) + 1]; }
first<N extends T>(ctor: {new (...args: any[]): N}): N|undefined {
for (let i = this.path.length - 1; i >= 0; i--) {
let item = this.path[i];
if (item instanceof ctor) return <N>item;
}
}
push(node: T) { this.path.push(node); }
pop(): T { return this.path.pop(); }
}

View File

@ -0,0 +1,47 @@
/**
* @license
* Copyright Google Inc. 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 {CompileDirectiveMetadata, CompileDirectiveSummary, CompilePipeSummary, CssSelector, Node as HtmlAst, ParseError, Parser, TemplateAst} from '@angular/compiler';
import {Diagnostic, TemplateSource} from './types';
export interface AstResult {
htmlAst?: HtmlAst[];
templateAst?: TemplateAst[];
directive?: CompileDirectiveMetadata;
directives?: CompileDirectiveSummary[];
pipes?: CompilePipeSummary[];
parseErrors?: ParseError[];
expressionParser?: Parser;
errors?: Diagnostic[];
}
export interface TemplateInfo {
position?: number;
fileName?: string;
template: TemplateSource;
htmlAst: HtmlAst[];
directive: CompileDirectiveMetadata;
directives: CompileDirectiveSummary[];
pipes: CompilePipeSummary[];
templateAst: TemplateAst[];
expressionParser: Parser;
}
export interface AttrInfo {
name: string;
input?: boolean;
output?: boolean;
template?: boolean;
fromHtml?: boolean;
}
export type SelectorInfo = {
selectors: CssSelector[],
map: Map<CssSelector, CompileDirectiveSummary>
};

View File

@ -0,0 +1,490 @@
/**
* @license
* Copyright Google Inc. 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 {AST, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, CssSelector, DirectiveAst, Element, ElementAst, EmbeddedTemplateAst, ImplicitReceiver, NAMED_ENTITIES, NgContentAst, Node as HtmlAst, ParseSpan, PropertyRead, ReferenceAst, SelectorMatcher, TagContentType, TemplateAst, TemplateAstVisitor, Text, TextAst, VariableAst, getHtmlTagDefinition, splitNsName, templateVisitAll} from '@angular/compiler';
import {AstResult, AttrInfo, SelectorInfo, TemplateInfo} from './common';
import {getExpressionCompletions, getExpressionScope} from './expressions';
import {attributeNames, elementNames, eventNames, propertyNames} from './html_info';
import {HtmlAstPath} from './html_path';
import {NullTemplateVisitor, TemplateAstChildVisitor, TemplateAstPath} from './template_path';
import {BuiltinType, Completion, Completions, Span, Symbol, SymbolDeclaration, SymbolTable, TemplateSource} from './types';
import {flatten, getSelectors, hasTemplateReference, inSpan, removeSuffix, spanOf, uniqueByName} from './utils';
const TEMPLATE_ATTR_PREFIX = '*';
const hiddenHtmlElements = {
html: true,
script: true,
noscript: true,
base: true,
body: true,
title: true,
head: true,
link: true,
};
export function getTemplateCompletions(templateInfo: TemplateInfo): Completions {
let result: Completions = undefined;
let {htmlAst, templateAst, template} = templateInfo;
// The templateNode starts at the delimiter character so we add 1 to skip it.
let templatePosition = templateInfo.position - template.span.start;
let path = new HtmlAstPath(htmlAst, templatePosition);
let mostSpecific = path.tail;
if (path.empty) {
result = elementCompletions(templateInfo, path);
} else {
let astPosition = templatePosition - mostSpecific.sourceSpan.start.offset;
mostSpecific.visit(
{
visitElement(ast) {
let startTagSpan = spanOf(ast.sourceSpan);
let tagLen = ast.name.length;
if (templatePosition <=
startTagSpan.start + tagLen + 1 /* 1 for the opening angle bracked */) {
// If we are in the tag then return the element completions.
result = elementCompletions(templateInfo, path);
} else if (templatePosition < startTagSpan.end) {
// We are in the attribute section of the element (but not in an attribute).
// Return the attribute completions.
result = attributeCompletions(templateInfo, path);
}
},
visitAttribute(ast) {
if (!ast.valueSpan || !inSpan(templatePosition, spanOf(ast.valueSpan))) {
// We are in the name of an attribute. Show attribute completions.
result = attributeCompletions(templateInfo, path);
} else if (ast.valueSpan && inSpan(templatePosition, spanOf(ast.valueSpan))) {
result = attributeValueCompletions(templateInfo, templatePosition, ast);
}
},
visitText(ast) {
// Check if we are in a entity.
result = entityCompletions(getSourceText(template, spanOf(ast)), astPosition);
if (result) return result;
result = interpolationCompletions(templateInfo, templatePosition);
if (result) return result;
let element = path.first(Element);
if (element) {
let definition = getHtmlTagDefinition(element.name);
if (definition.contentType === TagContentType.PARSABLE_DATA) {
result = voidElementAttributeCompletions(templateInfo, path);
if (!result) {
// If the element can hold content Show element completions.
result = elementCompletions(templateInfo, path);
}
}
} else {
// If no element container, implies parsable data so show elements.
result = voidElementAttributeCompletions(templateInfo, path);
if (!result) {
result = elementCompletions(templateInfo, path);
}
}
},
visitComment(ast) {},
visitExpansion(ast) {},
visitExpansionCase(ast) {}
},
null);
}
return result;
}
function attributeCompletions(info: TemplateInfo, path: HtmlAstPath): Completions {
let item = path.tail instanceof Element ? path.tail : path.parentOf(path.tail);
if (item instanceof Element) {
return attributeCompletionsForElement(info, item.name, item);
}
return undefined;
}
function attributeCompletionsForElement(
info: TemplateInfo, elementName: string, element?: Element): Completions {
const attributes = getAttributeInfosForElement(info, elementName, element);
// Map all the attributes to a completion
return attributes.map<Completion>(attr => ({
kind: attr.fromHtml ? 'html attribute' : 'attribute',
name: nameOfAttr(attr),
sort: attr.name
}));
}
function getAttributeInfosForElement(
info: TemplateInfo, elementName: string, element?: Element): AttrInfo[] {
let attributes: AttrInfo[] = [];
// Add html attributes
let htmlAttributes = attributeNames(elementName) || [];
if (htmlAttributes) {
attributes.push(...htmlAttributes.map<AttrInfo>(name => ({name, fromHtml: true})));
}
// Add html properties
let htmlProperties = propertyNames(elementName);
if (htmlProperties) {
attributes.push(...htmlProperties.map<AttrInfo>(name => ({name, input: true})));
}
// Add html events
let htmlEvents = eventNames(elementName);
if (htmlEvents) {
attributes.push(...htmlEvents.map<AttrInfo>(name => ({name, output: true})));
}
let {selectors, map: selectorMap} = getSelectors(info);
if (selectors && selectors.length) {
// All the attributes that are selectable should be shown.
const applicableSelectors =
selectors.filter(selector => !selector.element || selector.element == elementName);
const selectorAndAttributeNames =
applicableSelectors.map(selector => ({selector, attrs: selector.attrs.filter(a => !!a)}));
let attrs = flatten(selectorAndAttributeNames.map<AttrInfo[]>(selectorAndAttr => {
const directive = selectorMap.get(selectorAndAttr.selector);
const result = selectorAndAttr.attrs.map<AttrInfo>(
name => ({name, input: name in directive.inputs, output: name in directive.outputs}));
return result;
}));
// Add template attribute if a directive contains a template reference
selectorAndAttributeNames.forEach(selectorAndAttr => {
const selector = selectorAndAttr.selector;
const directive = selectorMap.get(selector);
if (directive && hasTemplateReference(directive.type) && selector.attrs.length &&
selector.attrs[0]) {
attrs.push({name: selector.attrs[0], template: true});
}
});
// All input and output properties of the matching directives should be added.
let elementSelector = element ?
createElementCssSelector(element) :
createElementCssSelector(new Element(elementName, [], [], undefined, undefined, undefined));
let matcher = new SelectorMatcher();
matcher.addSelectables(selectors);
matcher.match(elementSelector, selector => {
let directive = selectorMap.get(selector);
if (directive) {
attrs.push(...Object.keys(directive.inputs).map(name => ({name, input: true})));
attrs.push(...Object.keys(directive.outputs).map(name => ({name, output: true})));
}
});
// If a name shows up twice, fold it into a single value.
attrs = foldAttrs(attrs);
// Now expand them back out to ensure that input/output shows up as well as input and
// output.
attributes.push(...flatten(attrs.map(expandedAttr)));
}
return attributes;
}
function attributeValueCompletions(
info: TemplateInfo, position: number, attr: Attribute): Completions {
const path = new TemplateAstPath(info.templateAst, position);
const mostSpecific = path.tail;
if (mostSpecific) {
const visitor =
new ExpressionVisitor(info, position, attr, () => getExpressionScope(info, path, false));
mostSpecific.visit(visitor, null);
if (!visitor.result || !visitor.result.length) {
// Try allwoing widening the path
const widerPath = new TemplateAstPath(info.templateAst, position, /* allowWidening */ true);
if (widerPath.tail) {
const widerVisitor = new ExpressionVisitor(
info, position, attr, () => getExpressionScope(info, widerPath, false));
widerPath.tail.visit(widerVisitor, null);
return widerVisitor.result;
}
}
return visitor.result;
}
}
function elementCompletions(info: TemplateInfo, path: HtmlAstPath): Completions {
let htmlNames = elementNames().filter(name => !(name in hiddenHtmlElements));
// Collect the elements referenced by the selectors
let directiveElements =
getSelectors(info).selectors.map(selector => selector.element).filter(name => !!name);
let components =
directiveElements.map<Completion>(name => ({kind: 'component', name: name, sort: name}));
let htmlElements = htmlNames.map<Completion>(name => ({kind: 'element', name: name, sort: name}));
// Return components and html elements
return uniqueByName(htmlElements.concat(components));
}
function entityCompletions(value: string, position: number): Completions {
// Look for entity completions
const re = /&[A-Za-z]*;?(?!\d)/g;
let found: RegExpExecArray|null;
let result: Completions;
while (found = re.exec(value)) {
let len = found[0].length;
if (position >= found.index && position < (found.index + len)) {
result = Object.keys(NAMED_ENTITIES)
.map<Completion>(name => ({kind: 'entity', name: `&${name};`, sort: name}));
break;
}
}
return result;
}
function interpolationCompletions(info: TemplateInfo, position: number): Completions {
// Look for an interpolation in at the position.
const templatePath = new TemplateAstPath(info.templateAst, position);
const mostSpecific = templatePath.tail;
if (mostSpecific) {
let visitor = new ExpressionVisitor(
info, position, undefined, () => getExpressionScope(info, templatePath, false));
mostSpecific.visit(visitor, null);
return uniqueByName(visitor.result);
}
}
// There is a special case of HTML where text that contains a unclosed tag is treated as
// text. For exaple '<h1> Some <a text </h1>' produces a text nodes inside of the H1
// element "Some <a text". We, however, want to treat this as if the user was requesting
// the attributes of an "a" element, not requesting completion in the a text element. This
// code checks for this case and returns element completions if it is detected or undefined
// if it is not.
function voidElementAttributeCompletions(info: TemplateInfo, path: HtmlAstPath): Completions {
let tail = path.tail;
if (tail instanceof Text) {
let match = tail.value.match(/<(\w(\w|\d|-)*:)?(\w(\w|\d|-)*)\s/);
// The position must be after the match, otherwise we are still in a place where elements
// are expected (such as `<|a` or `<a|`; we only want attributes for `<a |` or after).
if (match && path.position >= match.index + match[0].length + tail.sourceSpan.start.offset) {
return attributeCompletionsForElement(info, match[3]);
}
}
}
class ExpressionVisitor extends NullTemplateVisitor {
result: Completions;
constructor(
private info: TemplateInfo, private position: number, private attr?: Attribute,
private getExpressionScope?: () => SymbolTable) {
super();
if (!getExpressionScope) {
this.getExpressionScope = () => info.template.members;
}
}
visitDirectiveProperty(ast: BoundDirectivePropertyAst): void {
this.attributeValueCompletions(ast.value);
}
visitElementProperty(ast: BoundElementPropertyAst): void {
this.attributeValueCompletions(ast.value);
}
visitEvent(ast: BoundEventAst): void { this.attributeValueCompletions(ast.handler); }
visitElement(ast: ElementAst): void {
if (this.attr && getSelectors(this.info) && this.attr.name.startsWith(TEMPLATE_ATTR_PREFIX)) {
// The value is a template expression but the expression AST was not produced when the
// TemplateAst was produce so
// do that now.
const key = this.attr.name.substr(TEMPLATE_ATTR_PREFIX.length);
// Find the selector
const selectorInfo = getSelectors(this.info);
const selectors = selectorInfo.selectors;
const selector =
selectors.filter(s => s.attrs.some((attr, i) => i % 2 == 0 && attr == key))[0];
const templateBindingResult =
this.info.expressionParser.parseTemplateBindings(key, this.attr.value, null);
// find the template binding that contains the position
const valueRelativePosition = this.position - this.attr.valueSpan.start.offset - 1;
const bindings = templateBindingResult.templateBindings;
const binding =
bindings.find(
binding => inSpan(valueRelativePosition, binding.span, /* exclusive */ true)) ||
bindings.find(binding => inSpan(valueRelativePosition, binding.span));
const keyCompletions = () => {
let keys: string[] = [];
if (selector) {
const attrNames = selector.attrs.filter((_, i) => i % 2 == 0);
keys = attrNames.filter(name => name.startsWith(key) && name != key)
.map(name => lowerName(name.substr(key.length)));
}
keys.push('let');
this.result = keys.map(key => <Completion>{kind: 'key', name: key, sort: key});
};
if (!binding || (binding.key == key && !binding.expression)) {
// We are in the root binding. We should return `let` and keys that are left in the
// selector.
keyCompletions();
} else if (binding.keyIsVar) {
const equalLocation = this.attr.value.indexOf('=');
this.result = [];
if (equalLocation >= 0 && valueRelativePosition >= equalLocation) {
// We are after the '=' in a let clause. The valid values here are the members of the
// template reference's type parameter.
const directiveMetadata = selectorInfo.map.get(selector);
const contextTable =
this.info.template.query.getTemplateContext(directiveMetadata.type.reference);
if (contextTable) {
this.result = this.symbolsToCompletions(contextTable.values());
}
} else if (binding.key && valueRelativePosition <= (binding.key.length - key.length)) {
keyCompletions();
}
} else {
// If the position is in the expression or after the key or there is no key, return the
// expression completions
if ((binding.expression && inSpan(valueRelativePosition, binding.expression.ast.span)) ||
(binding.key &&
valueRelativePosition > binding.span.start + (binding.key.length - key.length)) ||
!binding.key) {
const span = new ParseSpan(0, this.attr.value.length);
this.attributeValueCompletions(
binding.expression ? binding.expression.ast :
new PropertyRead(span, new ImplicitReceiver(span), ''),
valueRelativePosition);
} else {
keyCompletions();
}
}
}
}
visitBoundText(ast: BoundTextAst) {
const expressionPosition = this.position - ast.sourceSpan.start.offset;
if (inSpan(expressionPosition, ast.value.span)) {
const completions = getExpressionCompletions(
this.getExpressionScope(), ast.value, expressionPosition, this.info.template.query);
if (completions) {
this.result = this.symbolsToCompletions(completions);
}
}
}
private attributeValueCompletions(value: AST, position?: number) {
const symbols = getExpressionCompletions(
this.getExpressionScope(), value, position == null ? this.attributeValuePosition : position,
this.info.template.query);
if (symbols) {
this.result = this.symbolsToCompletions(symbols);
}
}
private symbolsToCompletions(symbols: Symbol[]): Completions {
return symbols.filter(s => !s.name.startsWith('__') && s.public)
.map(symbol => <Completion>{kind: symbol.kind, name: symbol.name, sort: symbol.name});
}
private get attributeValuePosition() {
return this.position - this.attr.valueSpan.start.offset - 1;
}
}
function getSourceText(template: TemplateSource, span: Span): string {
return template.source.substring(span.start, span.end);
}
function nameOfAttr(attr: AttrInfo): string {
let name = attr.name;
if (attr.output) {
name = removeSuffix(name, 'Events');
name = removeSuffix(name, 'Changed');
}
let result = [name];
if (attr.input) {
result.unshift('[');
result.push(']');
}
if (attr.output) {
result.unshift('(');
result.push(')');
}
if (attr.template) {
result.unshift('*');
}
return result.join('');
}
const templateAttr = /^(\w+:)?(template$|^\*)/;
function createElementCssSelector(element: Element): CssSelector {
const cssSelector = new CssSelector();
let elNameNoNs = splitNsName(element.name)[1];
cssSelector.setElement(elNameNoNs);
for (let attr of element.attrs) {
if (!attr.name.match(templateAttr)) {
let [_, attrNameNoNs] = splitNsName(attr.name);
cssSelector.addAttribute(attrNameNoNs, attr.value);
if (attr.name.toLowerCase() == 'class') {
const classes = attr.value.split(/s+/g);
classes.forEach(className => cssSelector.addClassName(className));
}
}
}
return cssSelector;
}
function foldAttrs(attrs: AttrInfo[]): AttrInfo[] {
let inputOutput = new Map<string, AttrInfo>();
let templates = new Map<string, AttrInfo>();
let result: AttrInfo[] = [];
attrs.forEach(attr => {
if (attr.fromHtml) {
return attr;
}
if (attr.template) {
let duplicate = templates.get(attr.name);
if (!duplicate) {
result.push({name: attr.name, template: true});
templates.set(attr.name, attr);
}
}
if (attr.input || attr.output) {
let duplicate = inputOutput.get(attr.name);
if (duplicate) {
duplicate.input = duplicate.input || attr.input;
duplicate.output = duplicate.output || attr.output;
} else {
let cloneAttr: AttrInfo = {name: attr.name};
if (attr.input) cloneAttr.input = true;
if (attr.output) cloneAttr.output = true;
result.push(cloneAttr);
inputOutput.set(attr.name, cloneAttr);
}
}
});
return result;
}
function expandedAttr(attr: AttrInfo): AttrInfo[] {
if (attr.input && attr.output) {
return [
attr, {name: attr.name, input: true, output: false},
{name: attr.name, input: false, output: true}
];
}
return [attr];
}
function lowerName(name: string): string {
return name && (name[0].toLowerCase() + name.substr(1));
}

View File

@ -0,0 +1,16 @@
/**
* @license
* Copyright Google Inc. 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 {TemplateInfo} from './common';
import {locateSymbol} from './locate_symbol';
import {Definition} from './types';
export function getDefinition(info: TemplateInfo): Definition {
const result = locateSymbol(info);
return result && result.symbol.definition;
}

View File

@ -0,0 +1,249 @@
/**
* @license
* Copyright Google Inc. 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 {AST, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, CompileDirectiveMetadata, CompileDirectiveSummary, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgAnalyzedModules, NgContentAst, ReferenceAst, StaticSymbol, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '@angular/compiler';
import {AstResult, SelectorInfo, TemplateInfo} from './common';
import {getExpressionDiagnostics, getExpressionScope} from './expressions';
import {HtmlAstPath} from './html_path';
import {NullTemplateVisitor, TemplateAstChildVisitor, TemplateAstPath} from './template_path';
import {Declaration, Declarations, Diagnostic, DiagnosticKind, Diagnostics, Span, SymbolTable, TemplateSource} from './types';
import {getSelectors, hasTemplateReference, offsetSpan, spanOf} from './utils';
export interface AstProvider {
getTemplateAst(template: TemplateSource, fileName: string): AstResult;
}
export function getTemplateDiagnostics(
fileName: string, astProvider: AstProvider, templates: TemplateSource[]): Diagnostics {
const results: Diagnostics = [];
for (const template of templates) {
const ast = astProvider.getTemplateAst(template, fileName);
if (ast) {
if (ast.parseErrors && ast.parseErrors.length) {
results.push(...ast.parseErrors.map<Diagnostic>(
e => ({
kind: DiagnosticKind.Error,
span: offsetSpan(spanOf(e.span), template.span.start),
message: e.msg
})));
} else if (ast.templateAst) {
const expressionDiagnostics = getTemplateExpressionDiagnostics(template, ast);
results.push(...expressionDiagnostics);
}
if (ast.errors) {
results.push(...ast.errors.map<Diagnostic>(
e => ({kind: e.kind, span: e.span || template.span, message: e.message})));
}
}
}
return results;
}
export function getDeclarationDiagnostics(
declarations: Declarations, modules: NgAnalyzedModules): Diagnostics {
const results: Diagnostics = [];
let directives: Set<StaticSymbol>|undefined = undefined;
for (const declaration of declarations) {
const report = (message: string, span?: Span) => {
results.push(<Diagnostic>{
kind: DiagnosticKind.Error,
span: span || declaration.declarationSpan, message
});
};
for (const error of declaration.errors) {
report(error.message, error.span);
}
if (declaration.metadata) {
if (declaration.metadata.isComponent) {
if (!modules.ngModuleByPipeOrDirective.has(declaration.type)) {
report(
`Component '${declaration.type.name}' is not included in a module and will not be available inside a template. Consider adding it to a NgModule declaration`);
}
if (declaration.metadata.template.template == null &&
!declaration.metadata.template.templateUrl) {
report(`Component ${declaration.type.name} must have a template or templateUrl`);
}
} else {
if (!directives) {
directives = new Set();
modules.ngModules.forEach(module => {
module.declaredDirectives.forEach(
directive => { directives.add(directive.reference); });
});
}
if (!directives.has(declaration.type)) {
report(
`Directive '${declaration.type.name}' is not included in a module and will not be available inside a template. Consider adding it to a NgModule declaration`);
}
}
}
}
return results;
}
function getTemplateExpressionDiagnostics(
template: TemplateSource, astResult: AstResult): Diagnostics {
const info: TemplateInfo = {
template,
htmlAst: astResult.htmlAst,
directive: astResult.directive,
directives: astResult.directives,
pipes: astResult.pipes,
templateAst: astResult.templateAst,
expressionParser: astResult.expressionParser
};
const visitor = new ExpressionDiagnosticsVisitor(
info, (path: TemplateAstPath, includeEvent: boolean) =>
getExpressionScope(info, path, includeEvent));
templateVisitAll(visitor, astResult.templateAst);
return visitor.diagnostics;
}
class ExpressionDiagnosticsVisitor extends TemplateAstChildVisitor {
private path: TemplateAstPath;
private directiveSummary: CompileDirectiveSummary;
diagnostics: Diagnostics = [];
constructor(
private info: TemplateInfo,
private getExpressionScope: (path: TemplateAstPath, includeEvent: boolean) => SymbolTable) {
super();
this.path = new TemplateAstPath([], 0);
}
visitDirective(ast: DirectiveAst, context: any): any {
// Override the default child visitor to ignore the host properties of a directive.
if (ast.inputs && ast.inputs.length) {
templateVisitAll(this, ast.inputs, context);
}
}
visitBoundText(ast: BoundTextAst): void {
this.push(ast);
this.diagnoseExpression(ast.value, ast.sourceSpan.start.offset, false);
this.pop();
}
visitDirectiveProperty(ast: BoundDirectivePropertyAst): void {
this.push(ast);
this.diagnoseExpression(ast.value, this.attributeValueLocation(ast), false);
this.pop();
}
visitElementProperty(ast: BoundElementPropertyAst): void {
this.push(ast);
this.diagnoseExpression(ast.value, this.attributeValueLocation(ast), false);
this.pop();
}
visitEvent(ast: BoundEventAst): void {
this.push(ast);
this.diagnoseExpression(ast.handler, this.attributeValueLocation(ast), true);
this.pop();
}
visitVariable(ast: VariableAst): void {
const directive = this.directiveSummary;
if (directive && ast.value) {
const context = this.info.template.query.getTemplateContext(directive.type.reference);
if (!context.has(ast.value)) {
if (ast.value === '$implicit') {
this.reportError(
'The template context does not have an implicit value', spanOf(ast.sourceSpan));
} else {
this.reportError(
`The template context does not defined a member called '${ast.value}'`,
spanOf(ast.sourceSpan));
}
}
}
}
visitElement(ast: ElementAst, context: any): void {
this.push(ast);
super.visitElement(ast, context);
this.pop();
}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
const previousDirectiveSummary = this.directiveSummary;
this.push(ast);
// Find directive that refernces this template
this.directiveSummary =
ast.directives.map(d => d.directive).find(d => hasTemplateReference(d.type));
// Process children
super.visitEmbeddedTemplate(ast, context);
this.pop();
this.directiveSummary = previousDirectiveSummary;
}
private attributeValueLocation(ast: TemplateAst) {
const path = new HtmlAstPath(this.info.htmlAst, ast.sourceSpan.start.offset);
const last = path.tail;
if (last instanceof Attribute && last.valueSpan) {
// Add 1 for the quote.
return last.valueSpan.start.offset + 1;
}
return ast.sourceSpan.start.offset;
}
private diagnoseExpression(ast: AST, offset: number, includeEvent: boolean) {
const scope = this.getExpressionScope(this.path, includeEvent);
this.diagnostics.push(
...getExpressionDiagnostics(scope, ast, this.info.template.query, {
event: includeEvent
}).map(d => ({
span: offsetSpan(d.ast.span, offset + this.info.template.span.start),
kind: d.kind,
message: d.message
})));
}
private push(ast: TemplateAst) { this.path.push(ast); }
private pop() { this.path.pop(); }
private _selectors: SelectorInfo;
private selectors(): SelectorInfo {
let result = this._selectors;
if (!result) {
this._selectors = result = getSelectors(this.info);
}
return result;
}
private findElement(position: number): Element {
const htmlPath = new HtmlAstPath(this.info.htmlAst, position);
if (htmlPath.tail instanceof Element) {
return htmlPath.tail;
}
}
private reportError(message: string, span: Span) {
this.diagnostics.push({
span: offsetSpan(span, this.info.template.span.start),
kind: DiagnosticKind.Error, message
});
}
private reportWarning(message: string, span: Span) {
this.diagnostics.push({
span: offsetSpan(span, this.info.template.span.start),
kind: DiagnosticKind.Warning, message
});
}
}

View File

@ -0,0 +1,785 @@
/**
* @license
* Copyright Google Inc. 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 {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, ElementAst, EmbeddedTemplateAst, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, PrefixNot, PropertyRead, PropertyWrite, Quote, ReferenceAst, SafeMethodCall, SafePropertyRead, StaticSymbol, TemplateAst, identifierName, templateVisitAll, tokenReference} from '@angular/compiler';
import {AstPath as AstPathBase} from './ast_path';
import {TemplateInfo} from './common';
import {TemplateAstChildVisitor, TemplateAstPath} from './template_path';
import {BuiltinType, CompletionKind, Definition, DiagnosticKind, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './types';
import {inSpan, spanOf} from './utils';
export interface ExpressionDiagnosticsContext { event?: boolean; }
export function getExpressionDiagnostics(
scope: SymbolTable, ast: AST, query: SymbolQuery,
context: ExpressionDiagnosticsContext = {}): TypeDiagnostic[] {
const analyzer = new AstType(scope, query, context);
analyzer.getDiagnostics(ast);
return analyzer.diagnostics;
}
export function getExpressionCompletions(
scope: SymbolTable, ast: AST, position: number, query: SymbolQuery): Symbol[] {
const path = new AstPath(ast, position);
if (path.empty) return undefined;
const tail = path.tail;
let result: SymbolTable|undefined = scope;
function getType(ast: AST): Symbol { return new AstType(scope, query, {}).getType(ast); }
// If the completion request is in a not in a pipe or property access then the global scope
// (that is the scope of the implicit receiver) is the right scope as the user is typing the
// beginning of an expression.
tail.visit({
visitBinary(ast) {},
visitChain(ast) {},
visitConditional(ast) {},
visitFunctionCall(ast) {},
visitImplicitReceiver(ast) {},
visitInterpolation(ast) { result = undefined; },
visitKeyedRead(ast) {},
visitKeyedWrite(ast) {},
visitLiteralArray(ast) {},
visitLiteralMap(ast) {},
visitLiteralPrimitive(ast) {},
visitMethodCall(ast) {},
visitPipe(ast) {
if (position >= ast.exp.span.end &&
(!ast.args || !ast.args.length || position < (<AST>ast.args[0]).span.start)) {
// We are in a position a pipe name is expected.
result = query.getPipes();
}
},
visitPrefixNot(ast) {},
visitPropertyRead(ast) {
const receiverType = getType(ast.receiver);
result = receiverType ? receiverType.members() : scope;
},
visitPropertyWrite(ast) {
const receiverType = getType(ast.receiver);
result = receiverType ? receiverType.members() : scope;
},
visitQuote(ast) {
// For a quote, return the members of any (if there are any).
result = query.getBuiltinType(BuiltinType.Any).members();
},
visitSafeMethodCall(ast) {
const receiverType = getType(ast.receiver);
result = receiverType ? receiverType.members() : scope;
},
visitSafePropertyRead(ast) {
const receiverType = getType(ast.receiver);
result = receiverType ? receiverType.members() : scope;
},
});
return result && result.values();
}
export function getExpressionSymbol(
scope: SymbolTable, ast: AST, position: number,
query: SymbolQuery): {symbol: Symbol, span: Span} {
const path = new AstPath(ast, position, /* excludeEmpty */ true);
if (path.empty) return undefined;
const tail = path.tail;
function getType(ast: AST): Symbol { return new AstType(scope, query, {}).getType(ast); }
let symbol: Symbol = undefined;
let span: Span = undefined;
// If the completion request is in a not in a pipe or property access then the global scope
// (that is the scope of the implicit receiver) is the right scope as the user is typing the
// beginning of an expression.
tail.visit({
visitBinary(ast) {},
visitChain(ast) {},
visitConditional(ast) {},
visitFunctionCall(ast) {},
visitImplicitReceiver(ast) {},
visitInterpolation(ast) {},
visitKeyedRead(ast) {},
visitKeyedWrite(ast) {},
visitLiteralArray(ast) {},
visitLiteralMap(ast) {},
visitLiteralPrimitive(ast) {},
visitMethodCall(ast) {
const receiverType = getType(ast.receiver);
symbol = receiverType && receiverType.members().get(ast.name);
span = ast.span;
},
visitPipe(ast) {
if (position >= ast.exp.span.end &&
(!ast.args || !ast.args.length || position < (<AST>ast.args[0]).span.start)) {
// We are in a position a pipe name is expected.
const pipes = query.getPipes();
if (pipes) {
symbol = pipes.get(ast.name);
span = ast.span;
}
}
},
visitPrefixNot(ast) {},
visitPropertyRead(ast) {
const receiverType = getType(ast.receiver);
symbol = receiverType && receiverType.members().get(ast.name);
span = ast.span;
},
visitPropertyWrite(ast) {
const receiverType = getType(ast.receiver);
symbol = receiverType && receiverType.members().get(ast.name);
span = ast.span;
},
visitQuote(ast) {},
visitSafeMethodCall(ast) {
const receiverType = getType(ast.receiver);
symbol = receiverType && receiverType.members().get(ast.name);
span = ast.span;
},
visitSafePropertyRead(ast) {
const receiverType = getType(ast.receiver);
symbol = receiverType && receiverType.members().get(ast.name);
span = ast.span;
},
});
if (symbol && span) {
return {symbol, span};
}
}
interface ExpressionVisitor extends AstVisitor {
visit?(ast: AST, context?: any): any;
}
// Consider moving to expression_parser/ast
class NullVisitor implements ExpressionVisitor {
visitBinary(ast: Binary): void {}
visitChain(ast: Chain): void {}
visitConditional(ast: Conditional): void {}
visitFunctionCall(ast: FunctionCall): void {}
visitImplicitReceiver(ast: ImplicitReceiver): void {}
visitInterpolation(ast: Interpolation): void {}
visitKeyedRead(ast: KeyedRead): void {}
visitKeyedWrite(ast: KeyedWrite): void {}
visitLiteralArray(ast: LiteralArray): void {}
visitLiteralMap(ast: LiteralMap): void {}
visitLiteralPrimitive(ast: LiteralPrimitive): void {}
visitMethodCall(ast: MethodCall): void {}
visitPipe(ast: BindingPipe): void {}
visitPrefixNot(ast: PrefixNot): void {}
visitPropertyRead(ast: PropertyRead): void {}
visitPropertyWrite(ast: PropertyWrite): void {}
visitQuote(ast: Quote): void {}
visitSafeMethodCall(ast: SafeMethodCall): void {}
visitSafePropertyRead(ast: SafePropertyRead): void {}
}
export class TypeDiagnostic {
constructor(public kind: DiagnosticKind, public message: string, public ast: AST) {}
}
// AstType calculatetype of the ast given AST element.
class AstType implements ExpressionVisitor {
public diagnostics: TypeDiagnostic[];
constructor(
private scope: SymbolTable, private query: SymbolQuery,
private context: ExpressionDiagnosticsContext) {}
getType(ast: AST): Symbol { return ast.visit(this); }
getDiagnostics(ast: AST): TypeDiagnostic[] {
this.diagnostics = [];
const type: Symbol = ast.visit(this);
if (this.context.event && type.callable) {
this.reportWarning('Unexpected callable expression. Expected a method call', ast);
}
return this.diagnostics;
}
visitBinary(ast: Binary): Symbol {
// Treat undefined and null as other.
function normalize(kind: BuiltinType, other: BuiltinType): BuiltinType {
switch (kind) {
case BuiltinType.Undefined:
case BuiltinType.Null:
return normalize(other, BuiltinType.Other);
}
return kind;
}
const leftType = this.getType(ast.left);
const rightType = this.getType(ast.right);
const leftRawKind = this.query.getTypeKind(leftType);
const rightRawKind = this.query.getTypeKind(rightType);
const leftKind = normalize(leftRawKind, rightRawKind);
const rightKind = normalize(rightRawKind, leftRawKind);
// The following swtich implements operator typing similar to the
// type production tables in the TypeScript specification.
// https://github.com/Microsoft/TypeScript/blob/v1.8.10/doc/spec.md#4.19
const operKind = leftKind << 8 | rightKind;
switch (ast.operation) {
case '*':
case '/':
case '%':
case '-':
case '<<':
case '>>':
case '>>>':
case '&':
case '^':
case '|':
switch (operKind) {
case BuiltinType.Any << 8 | BuiltinType.Any:
case BuiltinType.Number << 8 | BuiltinType.Any:
case BuiltinType.Any << 8 | BuiltinType.Number:
case BuiltinType.Number << 8 | BuiltinType.Number:
return this.query.getBuiltinType(BuiltinType.Number);
default:
let errorAst = ast.left;
switch (leftKind) {
case BuiltinType.Any:
case BuiltinType.Number:
errorAst = ast.right;
break;
}
return this.reportError('Expected a numeric type', errorAst);
}
case '+':
switch (operKind) {
case BuiltinType.Any << 8 | BuiltinType.Any:
case BuiltinType.Any << 8 | BuiltinType.Boolean:
case BuiltinType.Any << 8 | BuiltinType.Number:
case BuiltinType.Any << 8 | BuiltinType.Other:
case BuiltinType.Boolean << 8 | BuiltinType.Any:
case BuiltinType.Number << 8 | BuiltinType.Any:
case BuiltinType.Other << 8 | BuiltinType.Any:
return this.anyType;
case BuiltinType.Any << 8 | BuiltinType.String:
case BuiltinType.Boolean << 8 | BuiltinType.String:
case BuiltinType.Number << 8 | BuiltinType.String:
case BuiltinType.String << 8 | BuiltinType.Any:
case BuiltinType.String << 8 | BuiltinType.Boolean:
case BuiltinType.String << 8 | BuiltinType.Number:
case BuiltinType.String << 8 | BuiltinType.String:
case BuiltinType.String << 8 | BuiltinType.Other:
case BuiltinType.Other << 8 | BuiltinType.String:
return this.query.getBuiltinType(BuiltinType.String);
case BuiltinType.Number << 8 | BuiltinType.Number:
return this.query.getBuiltinType(BuiltinType.Number);
case BuiltinType.Boolean << 8 | BuiltinType.Number:
case BuiltinType.Other << 8 | BuiltinType.Number:
return this.reportError('Expected a number type', ast.left);
case BuiltinType.Number << 8 | BuiltinType.Boolean:
case BuiltinType.Number << 8 | BuiltinType.Other:
return this.reportError('Expected a number type', ast.right);
default:
return this.reportError('Expected operands to be a string or number type', ast);
}
case '>':
case '<':
case '<=':
case '>=':
case '==':
case '!=':
case '===':
case '!==':
switch (operKind) {
case BuiltinType.Any << 8 | BuiltinType.Any:
case BuiltinType.Any << 8 | BuiltinType.Boolean:
case BuiltinType.Any << 8 | BuiltinType.Number:
case BuiltinType.Any << 8 | BuiltinType.String:
case BuiltinType.Any << 8 | BuiltinType.Other:
case BuiltinType.Boolean << 8 | BuiltinType.Any:
case BuiltinType.Boolean << 8 | BuiltinType.Boolean:
case BuiltinType.Number << 8 | BuiltinType.Any:
case BuiltinType.Number << 8 | BuiltinType.Number:
case BuiltinType.String << 8 | BuiltinType.Any:
case BuiltinType.String << 8 | BuiltinType.String:
case BuiltinType.Other << 8 | BuiltinType.Any:
case BuiltinType.Other << 8 | BuiltinType.Other:
return this.query.getBuiltinType(BuiltinType.Boolean);
default:
return this.reportError('Expected the operants to be of similar type or any', ast);
}
case '&&':
return rightType;
case '||':
return this.query.getTypeUnion(leftType, rightType);
}
return this.reportError(`Unrecognized operator ${ast.operation}`, ast);
}
visitChain(ast: Chain) {
if (this.diagnostics) {
// If we are producing diagnostics, visit the children
visitChildren(ast, this);
}
// The type of a chain is always undefined.
return this.query.getBuiltinType(BuiltinType.Undefined);
}
visitConditional(ast: Conditional) {
// The type of a conditional is the union of the true and false conditions.
return this.query.getTypeUnion(this.getType(ast.trueExp), this.getType(ast.falseExp));
}
visitFunctionCall(ast: FunctionCall) {
// The type of a function call is the return type of the selected signature.
// The signature is selected based on the types of the arguments. Angular doesn't
// support contextual typing of arguments so this is simpler than TypeScript's
// version.
const args = ast.args.map(arg => this.getType(arg));
const target = this.getType(ast.target);
if (!target || !target.callable) return this.reportError('Call target is not callable', ast);
const signature = target.selectSignature(args);
if (signature) return signature.result;
// TODO: Consider a better error message here.
return this.reportError('Unable no compatible signature found for call', ast);
}
visitImplicitReceiver(ast: ImplicitReceiver): Symbol {
const _this = this;
// Return a pseudo-symbol for the implicit receiver.
// The members of the implicit receiver are what is defined by the
// scope passed into this class.
return {
name: '$implict',
kind: 'component',
language: 'ng-template',
type: undefined,
container: undefined,
callable: false,
public: true,
definition: undefined,
members(): SymbolTable{return _this.scope;},
signatures(): Signature[]{return [];},
selectSignature(types): Signature | undefined{return undefined;},
indexed(argument): Symbol | undefined{return undefined;}
};
}
visitInterpolation(ast: Interpolation): Symbol {
// If we are producing diagnostics, visit the children.
if (this.diagnostics) {
visitChildren(ast, this);
}
return this.undefinedType;
}
visitKeyedRead(ast: KeyedRead): Symbol {
const targetType = this.getType(ast.obj);
const keyType = this.getType(ast.key);
const result = targetType.indexed(keyType);
return result || this.anyType;
}
visitKeyedWrite(ast: KeyedWrite): Symbol {
// The write of a type is the type of the value being written.
return this.getType(ast.value);
}
visitLiteralArray(ast: LiteralArray): Symbol {
// A type literal is an array type of the union of the elements
return this.query.getArrayType(
this.query.getTypeUnion(...ast.expressions.map(element => this.getType(element))));
}
visitLiteralMap(ast: LiteralMap): Symbol {
// If we are producing diagnostics, visit the children
if (this.diagnostics) {
visitChildren(ast, this);
}
// TODO: Return a composite type.
return this.anyType;
}
visitLiteralPrimitive(ast: LiteralPrimitive) {
// The type of a literal primitive depends on the value of the literal.
switch (ast.value) {
case true:
case false:
return this.query.getBuiltinType(BuiltinType.Boolean);
case null:
return this.query.getBuiltinType(BuiltinType.Null);
case undefined:
return this.query.getBuiltinType(BuiltinType.Undefined);
default:
switch (typeof ast.value) {
case 'string':
return this.query.getBuiltinType(BuiltinType.String);
case 'number':
return this.query.getBuiltinType(BuiltinType.Number);
default:
return this.reportError('Unrecognized primitive', ast);
}
}
}
visitMethodCall(ast: MethodCall) {
return this.resolveMethodCall(this.getType(ast.receiver), ast);
}
visitPipe(ast: BindingPipe) {
// The type of a pipe node is the return type of the pipe's transform method. The table returned
// by getPipes() is expected to contain symbols with the corresponding transform method type.
const pipe = this.query.getPipes().get(ast.name);
if (!pipe) return this.reportError(`No pipe by the name ${pipe.name} found`, ast);
const expType = this.getType(ast.exp);
const signature =
pipe.selectSignature([expType].concat(ast.args.map(arg => this.getType(arg))));
if (!signature) return this.reportError('Unable to resolve signature for pipe invocation', ast);
return signature.result;
}
visitPrefixNot(ast: PrefixNot) {
// The type of a prefix ! is always boolean.
return this.query.getBuiltinType(BuiltinType.Boolean);
}
visitPropertyRead(ast: PropertyRead) {
return this.resolvePropertyRead(this.getType(ast.receiver), ast);
}
visitPropertyWrite(ast: PropertyWrite) {
// The type of a write is the type of the value being written.
return this.getType(ast.value);
}
visitQuote(ast: Quote) {
// The type of a quoted expression is any.
return this.query.getBuiltinType(BuiltinType.Any);
}
visitSafeMethodCall(ast: SafeMethodCall) {
return this.resolveMethodCall(this.query.getNonNullableType(this.getType(ast.receiver)), ast);
}
visitSafePropertyRead(ast: SafePropertyRead) {
return this.resolvePropertyRead(this.query.getNonNullableType(this.getType(ast.receiver)), ast);
}
private _anyType: Symbol;
private get anyType(): Symbol {
let result = this._anyType;
if (!result) {
result = this._anyType = this.query.getBuiltinType(BuiltinType.Any);
}
return result;
}
private _undefinedType: Symbol;
private get undefinedType(): Symbol {
let result = this._undefinedType;
if (!result) {
result = this._undefinedType = this.query.getBuiltinType(BuiltinType.Undefined);
}
return result;
}
private resolveMethodCall(receiverType: Symbol, ast: SafeMethodCall|MethodCall) {
if (this.isAny(receiverType)) {
return this.anyType;
}
// The type of a method is the selected methods result type.
const method = receiverType.members().get(ast.name);
if (!method) return this.reportError(`Unknown method ${ast.name}`, ast);
if (!method.type.callable) return this.reportError(`Member ${ast.name} is not callable`, ast);
const signature = method.type.selectSignature(ast.args.map(arg => this.getType(arg)));
if (!signature)
return this.reportError(`Unable to resolve signature for call of method ${ast.name}`, ast);
return signature.result;
}
private resolvePropertyRead(receiverType: Symbol, ast: SafePropertyRead|PropertyRead) {
if (this.isAny(receiverType)) {
return this.anyType;
}
// The type of a property read is the seelcted member's type.
const member = receiverType.members().get(ast.name);
if (!member) {
let receiverInfo = receiverType.name;
if (receiverInfo == '$implict') {
receiverInfo =
'The component declaration, template variable declarations, and element references do';
} else {
receiverInfo = `'${receiverInfo}' does`;
}
return this.reportError(
`Identifier '${ast.name}' is not defined. ${receiverInfo} not contain such a member`,
ast);
}
if (!member.public) {
let receiverInfo = receiverType.name;
if (receiverInfo == '$implict') {
receiverInfo = 'the component';
} else {
receiverInfo = `'${receiverInfo}'`;
}
this.reportWarning(
`Identifier '${ast.name}' refers to a private member of ${receiverInfo}`, ast);
}
return member.type;
}
private reportError(message: string, ast: AST): Symbol {
if (this.diagnostics) {
this.diagnostics.push(new TypeDiagnostic(DiagnosticKind.Error, message, ast));
}
return this.anyType;
}
private reportWarning(message: string, ast: AST): Symbol {
if (this.diagnostics) {
this.diagnostics.push(new TypeDiagnostic(DiagnosticKind.Warning, message, ast));
}
return this.anyType;
}
private isAny(symbol: Symbol): boolean {
return !symbol || this.query.getTypeKind(symbol) == BuiltinType.Any ||
(symbol.type && this.isAny(symbol.type));
}
}
class AstPath extends AstPathBase<AST> {
constructor(ast: AST, public position: number, excludeEmpty: boolean = false) {
super(new AstPathVisitor(position, excludeEmpty).buildPath(ast).path);
}
}
class AstPathVisitor extends NullVisitor {
public path: AST[] = [];
constructor(private position: number, private excludeEmpty: boolean) { super(); }
visit(ast: AST) {
if ((!this.excludeEmpty || ast.span.start < ast.span.end) && inSpan(this.position, ast.span)) {
this.path.push(ast);
visitChildren(ast, this);
}
}
buildPath(ast: AST): AstPathVisitor {
// We never care about the ASTWithSource node and its visit() method calls its ast's visit so
// the visit() method above would never see it.
if (ast instanceof ASTWithSource) {
ast = ast.ast;
}
this.visit(ast);
return this;
}
}
// TODO: Consider moving to expression_parser/ast
function visitChildren(ast: AST, visitor: ExpressionVisitor) {
function visit(ast: AST) { visitor.visit && visitor.visit(ast) || ast.visit(visitor); }
function visitAll<T extends AST>(asts: T[]) { asts.forEach(visit); }
ast.visit({
visitBinary(ast) {
visit(ast.left);
visit(ast.right);
},
visitChain(ast) { visitAll(ast.expressions); },
visitConditional(ast) {
visit(ast.condition);
visit(ast.trueExp);
visit(ast.falseExp);
},
visitFunctionCall(ast) {
visit(ast.target);
visitAll(ast.args);
},
visitImplicitReceiver(ast) {},
visitInterpolation(ast) { visitAll(ast.expressions); },
visitKeyedRead(ast) {
visit(ast.obj);
visit(ast.key);
},
visitKeyedWrite(ast) {
visit(ast.obj);
visit(ast.key);
visit(ast.obj);
},
visitLiteralArray(ast) { visitAll(ast.expressions); },
visitLiteralMap(ast) {},
visitLiteralPrimitive(ast) {},
visitMethodCall(ast) {
visit(ast.receiver);
visitAll(ast.args);
},
visitPipe(ast) {
visit(ast.exp);
visitAll(ast.args);
},
visitPrefixNot(ast) { visit(ast.expression); },
visitPropertyRead(ast) { visit(ast.receiver); },
visitPropertyWrite(ast) {
visit(ast.receiver);
visit(ast.value);
},
visitQuote(ast) {},
visitSafeMethodCall(ast) {
visit(ast.receiver);
visitAll(ast.args);
},
visitSafePropertyRead(ast) { visit(ast.receiver); },
});
}
export function getExpressionScope(
info: TemplateInfo, path: TemplateAstPath, includeEvent: boolean): SymbolTable {
let result = info.template.members;
const references = getReferences(info);
const variables = getVarDeclarations(info, path);
const events = getEventDeclaration(info, path, includeEvent);
if (references.length || variables.length || events.length) {
const referenceTable = info.template.query.createSymbolTable(references);
const variableTable = info.template.query.createSymbolTable(variables);
const eventsTable = info.template.query.createSymbolTable(events);
result =
info.template.query.mergeSymbolTable([result, referenceTable, variableTable, eventsTable]);
}
return result;
}
function getEventDeclaration(info: TemplateInfo, path: TemplateAstPath, includeEvent?: boolean) {
let result: SymbolDeclaration[] = [];
if (includeEvent) {
// TODO: Determine the type of the event parameter based on the Observable<T> or EventEmitter<T>
// of the event.
result = [{
name: '$event',
kind: 'variable',
type: info.template.query.getBuiltinType(BuiltinType.Any)
}];
}
return result;
}
function getReferences(info: TemplateInfo): SymbolDeclaration[] {
const result: SymbolDeclaration[] = [];
function processReferences(references: ReferenceAst[]) {
for (const reference of references) {
let type: Symbol;
if (reference.value) {
type = info.template.query.getTypeSymbol(tokenReference(reference.value));
}
result.push({
name: reference.name,
kind: 'reference',
type: type || info.template.query.getBuiltinType(BuiltinType.Any),
get definition() { return getDefintionOf(info, reference); }
});
}
}
const visitor = new class extends TemplateAstChildVisitor {
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
super.visitEmbeddedTemplate(ast, context);
processReferences(ast.references);
}
visitElement(ast: ElementAst, context: any): any {
super.visitElement(ast, context);
processReferences(ast.references);
}
};
templateVisitAll(visitor, info.templateAst);
return result;
}
function getVarDeclarations(info: TemplateInfo, path: TemplateAstPath): SymbolDeclaration[] {
const result: SymbolDeclaration[] = [];
let current = path.tail;
while (current) {
if (current instanceof EmbeddedTemplateAst) {
for (const variable of current.variables) {
const name = variable.name;
// Find the first directive with a context.
const context =
current.directives
.map(d => info.template.query.getTemplateContext(d.directive.type.reference))
.find(c => !!c);
// Determine the type of the context field referenced by variable.value.
let type: Symbol;
if (context) {
const value = context.get(variable.value);
if (value) {
type = value.type;
let kind = info.template.query.getTypeKind(type);
if (kind === BuiltinType.Any || kind == BuiltinType.Unbound) {
// The any type is not very useful here. For special cases, such as ngFor, we can do
// better.
type = refinedVariableType(type, info, current);
}
}
}
if (!type) {
type = info.template.query.getBuiltinType(BuiltinType.Any);
}
result.push({
name,
kind: 'variable', type, get definition() { return getDefintionOf(info, variable); }
});
}
}
current = path.parentOf(current);
}
return result;
}
function refinedVariableType(
type: Symbol, info: TemplateInfo, templateElement: EmbeddedTemplateAst): Symbol {
// Special case the ngFor directive
const ngForDirective = templateElement.directives.find(d => {
const name = identifierName(d.directive.type);
return name == 'NgFor' || name == 'NgForOf';
});
if (ngForDirective) {
const ngForOfBinding = ngForDirective.inputs.find(i => i.directiveName == 'ngForOf');
if (ngForOfBinding) {
const bindingType =
new AstType(info.template.members, info.template.query, {}).getType(ngForOfBinding.value);
if (bindingType) {
return info.template.query.getElementType(bindingType);
}
}
}
// We can't do better, just return the original type.
return type;
}
function getDefintionOf(info: TemplateInfo, ast: TemplateAst): Definition {
if (info.fileName) {
const templateOffset = info.template.span.start;
return [{
fileName: info.fileName,
span: {
start: ast.sourceSpan.start.offset + templateOffset,
end: ast.sourceSpan.end.offset + templateOffset
}
}];
}
}

View File

@ -0,0 +1,28 @@
/**
* @license
* Copyright Google Inc. 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 {TemplateInfo} from './common';
import {locateSymbol} from './locate_symbol';
import {Hover, HoverTextSection, Symbol} from './types';
export function getHover(info: TemplateInfo): Hover {
const result = locateSymbol(info);
if (result) {
return {text: hoverTextOf(result.symbol), span: result.span};
}
}
function hoverTextOf(symbol: Symbol): HoverTextSection[] {
const result: HoverTextSection[] =
[{text: symbol.kind}, {text: ' '}, {text: symbol.name, language: symbol.language}];
const container = symbol.container;
if (container) {
result.push({text: ' of '}, {text: container.name, language: container.language});
}
return result;
}

View File

@ -0,0 +1,462 @@
/**
* @license
* Copyright Google Inc. 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
*/
// Information about the HTML DOM elements
// This section defines the HTML elements and attribute surface of HTML 4
// which is derived from https://www.w3.org/TR/html4/strict.dtd
type attrType = string | string[];
type hash<T> = {
[name: string]: T
};
const values: attrType[] = [
'ID',
'CDATA',
'NAME',
['ltr', 'rtl'],
['rect', 'circle', 'poly', 'default'],
'NUMBER',
['nohref'],
['ismap'],
['declare'],
['DATA', 'REF', 'OBJECT'],
['GET', 'POST'],
'IDREF',
['TEXT', 'PASSWORD', 'CHECKBOX', 'RADIO', 'SUBMIT', 'RESET', 'FILE', 'HIDDEN', 'IMAGE', 'BUTTON'],
['checked'],
['disabled'],
['readonly'],
['multiple'],
['selected'],
['button', 'submit', 'reset'],
['void', 'above', 'below', 'hsides', 'lhs', 'rhs', 'vsides', 'box', 'border'],
['none', 'groups', 'rows', 'cols', 'all'],
['left', 'center', 'right', 'justify', 'char'],
['top', 'middle', 'bottom', 'baseline'],
'IDREFS',
['row', 'col', 'rowgroup', 'colgroup'],
['defer']
];
const groups: hash<number>[] = [
{id: 0},
{
onclick: 1,
ondblclick: 1,
onmousedown: 1,
onmouseup: 1,
onmouseover: 1,
onmousemove: 1,
onmouseout: 1,
onkeypress: 1,
onkeydown: 1,
onkeyup: 1
},
{lang: 2, dir: 3},
{onload: 1, onunload: 1},
{name: 1},
{href: 1},
{type: 1},
{alt: 1},
{tabindex: 5},
{media: 1},
{nohref: 6},
{usemap: 1},
{src: 1},
{onfocus: 1, onblur: 1},
{charset: 1},
{declare: 8, classid: 1, codebase: 1, data: 1, codetype: 1, archive: 1, standby: 1},
{title: 1},
{value: 1},
{cite: 1},
{datetime: 1},
{accept: 1},
{shape: 4, coords: 1},
{ for: 11
},
{action: 1, method: 10, enctype: 1, onsubmit: 1, onreset: 1, 'accept-charset': 1},
{valuetype: 9},
{longdesc: 1},
{width: 1},
{disabled: 14},
{readonly: 15, onselect: 1},
{accesskey: 1},
{size: 5, multiple: 16},
{onchange: 1},
{label: 1},
{selected: 17},
{type: 12, checked: 13, size: 1, maxlength: 5},
{rows: 5, cols: 5},
{type: 18},
{height: 1},
{summary: 1, border: 1, frame: 19, rules: 20, cellspacing: 1, cellpadding: 1, datapagesize: 1},
{align: 21, char: 1, charoff: 1, valign: 22},
{span: 5},
{abbr: 1, axis: 1, headers: 23, scope: 24, rowspan: 5, colspan: 5},
{profile: 1},
{'http-equiv': 2, name: 2, content: 1, scheme: 1},
{class: 1, style: 1},
{hreflang: 2, rel: 1, rev: 1},
{ismap: 7},
{ defer: 25, event: 1, for : 1 }
];
const elements: {[name: string]: number[]} = {
TT: [0, 1, 2, 16, 44],
I: [0, 1, 2, 16, 44],
B: [0, 1, 2, 16, 44],
BIG: [0, 1, 2, 16, 44],
SMALL: [0, 1, 2, 16, 44],
EM: [0, 1, 2, 16, 44],
STRONG: [0, 1, 2, 16, 44],
DFN: [0, 1, 2, 16, 44],
CODE: [0, 1, 2, 16, 44],
SAMP: [0, 1, 2, 16, 44],
KBD: [0, 1, 2, 16, 44],
VAR: [0, 1, 2, 16, 44],
CITE: [0, 1, 2, 16, 44],
ABBR: [0, 1, 2, 16, 44],
ACRONYM: [0, 1, 2, 16, 44],
SUB: [0, 1, 2, 16, 44],
SUP: [0, 1, 2, 16, 44],
SPAN: [0, 1, 2, 16, 44],
BDO: [0, 2, 16, 44],
BR: [0, 16, 44],
BODY: [0, 1, 2, 3, 16, 44],
ADDRESS: [0, 1, 2, 16, 44],
DIV: [0, 1, 2, 16, 44],
A: [0, 1, 2, 4, 5, 6, 8, 13, 14, 16, 21, 29, 44, 45],
MAP: [0, 1, 2, 4, 16, 44],
AREA: [0, 1, 2, 5, 7, 8, 10, 13, 16, 21, 29, 44],
LINK: [0, 1, 2, 5, 6, 9, 14, 16, 44, 45],
IMG: [0, 1, 2, 4, 7, 11, 12, 16, 25, 26, 37, 44, 46],
OBJECT: [0, 1, 2, 4, 6, 8, 11, 15, 16, 26, 37, 44],
PARAM: [0, 4, 6, 17, 24],
HR: [0, 1, 2, 16, 44],
P: [0, 1, 2, 16, 44],
H1: [0, 1, 2, 16, 44],
H2: [0, 1, 2, 16, 44],
H3: [0, 1, 2, 16, 44],
H4: [0, 1, 2, 16, 44],
H5: [0, 1, 2, 16, 44],
H6: [0, 1, 2, 16, 44],
PRE: [0, 1, 2, 16, 44],
Q: [0, 1, 2, 16, 18, 44],
BLOCKQUOTE: [0, 1, 2, 16, 18, 44],
INS: [0, 1, 2, 16, 18, 19, 44],
DEL: [0, 1, 2, 16, 18, 19, 44],
DL: [0, 1, 2, 16, 44],
DT: [0, 1, 2, 16, 44],
DD: [0, 1, 2, 16, 44],
OL: [0, 1, 2, 16, 44],
UL: [0, 1, 2, 16, 44],
LI: [0, 1, 2, 16, 44],
FORM: [0, 1, 2, 4, 16, 20, 23, 44],
LABEL: [0, 1, 2, 13, 16, 22, 29, 44],
INPUT: [0, 1, 2, 4, 7, 8, 11, 12, 13, 16, 17, 20, 27, 28, 29, 31, 34, 44, 46],
SELECT: [0, 1, 2, 4, 8, 13, 16, 27, 30, 31, 44],
OPTGROUP: [0, 1, 2, 16, 27, 32, 44],
OPTION: [0, 1, 2, 16, 17, 27, 32, 33, 44],
TEXTAREA: [0, 1, 2, 4, 8, 13, 16, 27, 28, 29, 31, 35, 44],
FIELDSET: [0, 1, 2, 16, 44],
LEGEND: [0, 1, 2, 16, 29, 44],
BUTTON: [0, 1, 2, 4, 8, 13, 16, 17, 27, 29, 36, 44],
TABLE: [0, 1, 2, 16, 26, 38, 44],
CAPTION: [0, 1, 2, 16, 44],
COLGROUP: [0, 1, 2, 16, 26, 39, 40, 44],
COL: [0, 1, 2, 16, 26, 39, 40, 44],
THEAD: [0, 1, 2, 16, 39, 44],
TBODY: [0, 1, 2, 16, 39, 44],
TFOOT: [0, 1, 2, 16, 39, 44],
TR: [0, 1, 2, 16, 39, 44],
TH: [0, 1, 2, 16, 39, 41, 44],
TD: [0, 1, 2, 16, 39, 41, 44],
HEAD: [2, 42],
TITLE: [2],
BASE: [5],
META: [2, 43],
STYLE: [2, 6, 9, 16],
SCRIPT: [6, 12, 14, 47],
NOSCRIPT: [0, 1, 2, 16, 44],
HTML: [2]
};
const defaultAttributes = [0, 1, 2, 4];
export function elementNames(): string[] {
return Object.keys(elements).sort().map(v => v.toLowerCase());
}
function compose(indexes: number[] | undefined): hash<attrType> {
const result: hash<attrType> = {};
if (indexes) {
for (let index of indexes) {
const group = groups[index];
for (let name in group)
if (group.hasOwnProperty(name)) result[name] = values[group[name]];
}
}
return result;
}
export function attributeNames(element: string): string[] {
return Object.keys(compose(elements[element.toUpperCase()] || defaultAttributes)).sort();
}
export function attributeType(element: string, attribute: string): string|string[]|undefined {
return compose(elements[element.toUpperCase()] || defaultAttributes)[attribute.toLowerCase()];
}
// This section is describes the DOM property surface of a DOM element and is dervided from
// from the SCHEMA strings from the security context information. SCHEMA is copied here because
// it would be an unnecessary risk to allow this array to be imported from the security context
// schema registry.
const SCHEMA:
string[] =
[
'[Element]|textContent,%classList,className,id,innerHTML,*beforecopy,*beforecut,*beforepaste,*copy,*cut,*paste,*search,*selectstart,*webkitfullscreenchange,*webkitfullscreenerror,*wheel,outerHTML,#scrollLeft,#scrollTop',
'[HTMLElement]^[Element]|accessKey,contentEditable,dir,!draggable,!hidden,innerText,lang,*abort,*beforecopy,*beforecut,*beforepaste,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*message,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*mozfullscreenchange,*mozfullscreenerror,*mozpointerlockchange,*mozpointerlockerror,*paste,*pause,*play,*playing,*progress,*ratechange,*reset,*resize,*scroll,*search,*seeked,*seeking,*select,*selectstart,*show,*stalled,*submit,*suspend,*timeupdate,*toggle,*volumechange,*waiting,*webglcontextcreationerror,*webglcontextlost,*webglcontextrestored,*webkitfullscreenchange,*webkitfullscreenerror,*wheel,outerText,!spellcheck,%style,#tabIndex,title,!translate',
'abbr,address,article,aside,b,bdi,bdo,cite,code,dd,dfn,dt,em,figcaption,figure,footer,header,i,kbd,main,mark,nav,noscript,rb,rp,rt,rtc,ruby,s,samp,section,small,strong,sub,sup,u,var,wbr^[HTMLElement]|accessKey,contentEditable,dir,!draggable,!hidden,innerText,lang,*abort,*beforecopy,*beforecut,*beforepaste,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*copy,*cuechange,*cut,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*message,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*mozfullscreenchange,*mozfullscreenerror,*mozpointerlockchange,*mozpointerlockerror,*paste,*pause,*play,*playing,*progress,*ratechange,*reset,*resize,*scroll,*search,*seeked,*seeking,*select,*selectstart,*show,*stalled,*submit,*suspend,*timeupdate,*toggle,*volumechange,*waiting,*webglcontextcreationerror,*webglcontextlost,*webglcontextrestored,*webkitfullscreenchange,*webkitfullscreenerror,*wheel,outerText,!spellcheck,%style,#tabIndex,title,!translate',
'media^[HTMLElement]|!autoplay,!controls,%crossOrigin,#currentTime,!defaultMuted,#defaultPlaybackRate,!disableRemotePlayback,!loop,!muted,*encrypted,#playbackRate,preload,src,%srcObject,#volume',
':svg:^[HTMLElement]|*abort,*blur,*cancel,*canplay,*canplaythrough,*change,*click,*close,*contextmenu,*cuechange,*dblclick,*drag,*dragend,*dragenter,*dragleave,*dragover,*dragstart,*drop,*durationchange,*emptied,*ended,*error,*focus,*input,*invalid,*keydown,*keypress,*keyup,*load,*loadeddata,*loadedmetadata,*loadstart,*mousedown,*mouseenter,*mouseleave,*mousemove,*mouseout,*mouseover,*mouseup,*mousewheel,*pause,*play,*playing,*progress,*ratechange,*reset,*resize,*scroll,*seeked,*seeking,*select,*show,*stalled,*submit,*suspend,*timeupdate,*toggle,*volumechange,*waiting,%style,#tabIndex',
':svg:graphics^:svg:|',
':svg:animation^:svg:|*begin,*end,*repeat',
':svg:geometry^:svg:|',
':svg:componentTransferFunction^:svg:|',
':svg:gradient^:svg:|',
':svg:textContent^:svg:graphics|',
':svg:textPositioning^:svg:textContent|',
'a^[HTMLElement]|charset,coords,download,hash,host,hostname,href,hreflang,name,password,pathname,ping,port,protocol,referrerPolicy,rel,rev,search,shape,target,text,type,username',
'area^[HTMLElement]|alt,coords,hash,host,hostname,href,!noHref,password,pathname,ping,port,protocol,referrerPolicy,search,shape,target,username',
'audio^media|',
'br^[HTMLElement]|clear',
'base^[HTMLElement]|href,target',
'body^[HTMLElement]|aLink,background,bgColor,link,*beforeunload,*blur,*error,*focus,*hashchange,*languagechange,*load,*message,*offline,*online,*pagehide,*pageshow,*popstate,*rejectionhandled,*resize,*scroll,*storage,*unhandledrejection,*unload,text,vLink',
'button^[HTMLElement]|!autofocus,!disabled,formAction,formEnctype,formMethod,!formNoValidate,formTarget,name,type,value',
'canvas^[HTMLElement]|#height,#width',
'content^[HTMLElement]|select',
'dl^[HTMLElement]|!compact',
'datalist^[HTMLElement]|',
'details^[HTMLElement]|!open',
'dialog^[HTMLElement]|!open,returnValue',
'dir^[HTMLElement]|!compact',
'div^[HTMLElement]|align',
'embed^[HTMLElement]|align,height,name,src,type,width',
'fieldset^[HTMLElement]|!disabled,name',
'font^[HTMLElement]|color,face,size',
'form^[HTMLElement]|acceptCharset,action,autocomplete,encoding,enctype,method,name,!noValidate,target',
'frame^[HTMLElement]|frameBorder,longDesc,marginHeight,marginWidth,name,!noResize,scrolling,src',
'frameset^[HTMLElement]|cols,*beforeunload,*blur,*error,*focus,*hashchange,*languagechange,*load,*message,*offline,*online,*pagehide,*pageshow,*popstate,*rejectionhandled,*resize,*scroll,*storage,*unhandledrejection,*unload,rows',
'hr^[HTMLElement]|align,color,!noShade,size,width',
'head^[HTMLElement]|',
'h1,h2,h3,h4,h5,h6^[HTMLElement]|align',
'html^[HTMLElement]|version',
'iframe^[HTMLElement]|align,!allowFullscreen,frameBorder,height,longDesc,marginHeight,marginWidth,name,referrerPolicy,%sandbox,scrolling,src,srcdoc,width',
'img^[HTMLElement]|align,alt,border,%crossOrigin,#height,#hspace,!isMap,longDesc,lowsrc,name,referrerPolicy,sizes,src,srcset,useMap,#vspace,#width',
'input^[HTMLElement]|accept,align,alt,autocapitalize,autocomplete,!autofocus,!checked,!defaultChecked,defaultValue,dirName,!disabled,%files,formAction,formEnctype,formMethod,!formNoValidate,formTarget,#height,!incremental,!indeterminate,max,#maxLength,min,#minLength,!multiple,name,pattern,placeholder,!readOnly,!required,selectionDirection,#selectionEnd,#selectionStart,#size,src,step,type,useMap,value,%valueAsDate,#valueAsNumber,#width',
'keygen^[HTMLElement]|!autofocus,challenge,!disabled,keytype,name',
'li^[HTMLElement]|type,#value',
'label^[HTMLElement]|htmlFor',
'legend^[HTMLElement]|align',
'link^[HTMLElement]|as,charset,%crossOrigin,!disabled,href,hreflang,integrity,media,rel,%relList,rev,%sizes,target,type',
'map^[HTMLElement]|name',
'marquee^[HTMLElement]|behavior,bgColor,direction,height,#hspace,#loop,#scrollAmount,#scrollDelay,!trueSpeed,#vspace,width',
'menu^[HTMLElement]|!compact',
'meta^[HTMLElement]|content,httpEquiv,name,scheme',
'meter^[HTMLElement]|#high,#low,#max,#min,#optimum,#value',
'ins,del^[HTMLElement]|cite,dateTime',
'ol^[HTMLElement]|!compact,!reversed,#start,type',
'object^[HTMLElement]|align,archive,border,code,codeBase,codeType,data,!declare,height,#hspace,name,standby,type,useMap,#vspace,width',
'optgroup^[HTMLElement]|!disabled,label',
'option^[HTMLElement]|!defaultSelected,!disabled,label,!selected,text,value',
'output^[HTMLElement]|defaultValue,%htmlFor,name,value',
'p^[HTMLElement]|align',
'param^[HTMLElement]|name,type,value,valueType',
'picture^[HTMLElement]|',
'pre^[HTMLElement]|#width',
'progress^[HTMLElement]|#max,#value',
'q,blockquote,cite^[HTMLElement]|',
'script^[HTMLElement]|!async,charset,%crossOrigin,!defer,event,htmlFor,integrity,src,text,type',
'select^[HTMLElement]|!autofocus,!disabled,#length,!multiple,name,!required,#selectedIndex,#size,value',
'shadow^[HTMLElement]|',
'source^[HTMLElement]|media,sizes,src,srcset,type',
'span^[HTMLElement]|',
'style^[HTMLElement]|!disabled,media,type',
'caption^[HTMLElement]|align',
'th,td^[HTMLElement]|abbr,align,axis,bgColor,ch,chOff,#colSpan,headers,height,!noWrap,#rowSpan,scope,vAlign,width',
'col,colgroup^[HTMLElement]|align,ch,chOff,#span,vAlign,width',
'table^[HTMLElement]|align,bgColor,border,%caption,cellPadding,cellSpacing,frame,rules,summary,%tFoot,%tHead,width',
'tr^[HTMLElement]|align,bgColor,ch,chOff,vAlign',
'tfoot,thead,tbody^[HTMLElement]|align,ch,chOff,vAlign',
'template^[HTMLElement]|',
'textarea^[HTMLElement]|autocapitalize,!autofocus,#cols,defaultValue,dirName,!disabled,#maxLength,#minLength,name,placeholder,!readOnly,!required,#rows,selectionDirection,#selectionEnd,#selectionStart,value,wrap',
'title^[HTMLElement]|text',
'track^[HTMLElement]|!default,kind,label,src,srclang',
'ul^[HTMLElement]|!compact,type',
'unknown^[HTMLElement]|',
'video^media|#height,poster,#width',
':svg:a^:svg:graphics|',
':svg:animate^:svg:animation|',
':svg:animateMotion^:svg:animation|',
':svg:animateTransform^:svg:animation|',
':svg:circle^:svg:geometry|',
':svg:clipPath^:svg:graphics|',
':svg:cursor^:svg:|',
':svg:defs^:svg:graphics|',
':svg:desc^:svg:|',
':svg:discard^:svg:|',
':svg:ellipse^:svg:geometry|',
':svg:feBlend^:svg:|',
':svg:feColorMatrix^:svg:|',
':svg:feComponentTransfer^:svg:|',
':svg:feComposite^:svg:|',
':svg:feConvolveMatrix^:svg:|',
':svg:feDiffuseLighting^:svg:|',
':svg:feDisplacementMap^:svg:|',
':svg:feDistantLight^:svg:|',
':svg:feDropShadow^:svg:|',
':svg:feFlood^:svg:|',
':svg:feFuncA^:svg:componentTransferFunction|',
':svg:feFuncB^:svg:componentTransferFunction|',
':svg:feFuncG^:svg:componentTransferFunction|',
':svg:feFuncR^:svg:componentTransferFunction|',
':svg:feGaussianBlur^:svg:|',
':svg:feImage^:svg:|',
':svg:feMerge^:svg:|',
':svg:feMergeNode^:svg:|',
':svg:feMorphology^:svg:|',
':svg:feOffset^:svg:|',
':svg:fePointLight^:svg:|',
':svg:feSpecularLighting^:svg:|',
':svg:feSpotLight^:svg:|',
':svg:feTile^:svg:|',
':svg:feTurbulence^:svg:|',
':svg:filter^:svg:|',
':svg:foreignObject^:svg:graphics|',
':svg:g^:svg:graphics|',
':svg:image^:svg:graphics|',
':svg:line^:svg:geometry|',
':svg:linearGradient^:svg:gradient|',
':svg:mpath^:svg:|',
':svg:marker^:svg:|',
':svg:mask^:svg:|',
':svg:metadata^:svg:|',
':svg:path^:svg:geometry|',
':svg:pattern^:svg:|',
':svg:polygon^:svg:geometry|',
':svg:polyline^:svg:geometry|',
':svg:radialGradient^:svg:gradient|',
':svg:rect^:svg:geometry|',
':svg:svg^:svg:graphics|#currentScale,#zoomAndPan',
':svg:script^:svg:|type',
':svg:set^:svg:animation|',
':svg:stop^:svg:|',
':svg:style^:svg:|!disabled,media,title,type',
':svg:switch^:svg:graphics|',
':svg:symbol^:svg:|',
':svg:tspan^:svg:textPositioning|',
':svg:text^:svg:textPositioning|',
':svg:textPath^:svg:textContent|',
':svg:title^:svg:|',
':svg:use^:svg:graphics|',
':svg:view^:svg:|#zoomAndPan',
'data^[HTMLElement]|value',
'menuitem^[HTMLElement]|type,label,icon,!disabled,!checked,radiogroup,!default',
'summary^[HTMLElement]|',
'time^[HTMLElement]|dateTime',
];
const attrToPropMap: {[name: string]: string} = <any>{
'class': 'className',
'formaction': 'formAction',
'innerHtml': 'innerHTML',
'readonly': 'readOnly',
'tabindex': 'tabIndex'
};
const EVENT = 'event';
const BOOLEAN = 'boolean';
const NUMBER = 'number';
const STRING = 'string';
const OBJECT = 'object';
export class SchemaInformation {
schema = <{[element: string]: {[property: string]: string}}>{};
constructor() {
SCHEMA.forEach(encodedType => {
const parts = encodedType.split('|');
const properties = parts[1].split(',');
const typeParts = (parts[0] + '^').split('^');
const typeName = typeParts[0];
const type = <{[property: string]: string}>{};
typeName.split(',').forEach(tag => this.schema[tag.toLowerCase()] = type);
const superName = typeParts[1];
const superType = superName && this.schema[superName.toLowerCase()];
if (superType) {
for (const key in superType) {
type[key] = superType[key];
}
}
properties.forEach((property: string) => {
if (property == '') {
} else if (property.startsWith('*')) {
type[property.substring(1)] = EVENT;
} else if (property.startsWith('!')) {
type[property.substring(1)] = BOOLEAN;
} else if (property.startsWith('#')) {
type[property.substring(1)] = NUMBER;
} else if (property.startsWith('%')) {
type[property.substring(1)] = OBJECT;
} else {
type[property] = STRING;
}
});
});
}
allKnownElements(): string[] { return Object.keys(this.schema); }
eventsOf(elementName: string): string[] {
const elementType = this.schema[elementName.toLowerCase()] || {};
return Object.keys(elementType).filter(property => elementType[property] === EVENT);
}
propertiesOf(elementName: string): string[] {
const elementType = this.schema[elementName.toLowerCase()] || {};
return Object.keys(elementType).filter(property => elementType[property] !== EVENT);
}
typeOf(elementName: string, property: string): string {
return (this.schema[elementName.toLowerCase()] || {})[property];
}
private static _instance: SchemaInformation;
static get instance(): SchemaInformation {
let result = SchemaInformation._instance;
if (!result) {
result = SchemaInformation._instance = new SchemaInformation();
}
return result;
}
}
export function eventNames(elementName: string): string[] {
return SchemaInformation.instance.eventsOf(elementName);
}
export function propertyNames(elementName: string): string[] {
return SchemaInformation.instance.propertiesOf(elementName);
}
export function propertyType(elementName: string, propertyName: string): string {
return SchemaInformation.instance.typeOf(elementName, propertyName);
}

View File

@ -0,0 +1,72 @@
/**
* @license
* Copyright Google Inc. 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 {Attribute, Comment, Element, Expansion, ExpansionCase, Node, Text, Visitor, visitAll} from '@angular/compiler';
import {AstPath} from './ast_path';
import {inSpan, spanOf} from './utils';
export class HtmlAstPath extends AstPath<Node> {
constructor(ast: Node[], public position: number) { super(buildPath(ast, position)); }
}
function buildPath(ast: Node[], position: number): Node[] {
let visitor = new HtmlAstPathBuilder(position);
visitAll(visitor, ast);
return visitor.getPath();
}
export class ChildVisitor implements Visitor {
constructor(private visitor?: Visitor) {}
visitElement(ast: Element, context: any): any {
this.visitChildren(context, visit => {
visit(ast.attrs);
visit(ast.children);
});
}
visitAttribute(ast: Attribute, context: any): any {}
visitText(ast: Text, context: any): any {}
visitComment(ast: Comment, context: any): any {}
visitExpansion(ast: Expansion, context: any): any {
return this.visitChildren(context, visit => { visit(ast.cases); });
}
visitExpansionCase(ast: ExpansionCase, context: any): any {}
private visitChildren<T extends Node>(
context: any, cb: (visit: (<V extends Node>(children: V[]|undefined) => void)) => void) {
const visitor = this.visitor || this;
let results: any[][] = [];
function visit<T extends Node>(children: T[] | undefined) {
if (children) results.push(visitAll(visitor, children, context));
}
cb(visit);
return [].concat.apply([], results);
}
}
class HtmlAstPathBuilder extends ChildVisitor {
private path: Node[] = [];
constructor(private position: number) { super(); }
visit(ast: Node, context: any): any {
let span = spanOf(ast);
if (inSpan(this.position, span)) {
this.path.push(ast);
} else {
// Returning a value here will result in the children being skipped.
return true;
}
}
getPath(): Node[] { return this.path; }
}

View File

@ -0,0 +1,184 @@
/**
* @license
* Copyright Google Inc. 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 {CompileMetadataResolver, CompileNgModuleMetadata, CompilerConfig, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgAnalyzedModules, Parser, TemplateParser} from '@angular/compiler';
import {AstResult, AttrInfo, TemplateInfo} from './common';
import {getTemplateCompletions} from './completions';
import {getDefinition} from './definitions';
import {getDeclarationDiagnostics, getTemplateDiagnostics} from './diagnostics';
import {getHover} from './hover';
import {Completion, CompletionKind, Completions, Declaration, Declarations, Definition, Diagnostic, DiagnosticKind, Diagnostics, Hover, LanguageService, LanguageServiceHost, Location, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable, TemplateSource, TemplateSources} from './types';
/**
* Create an instance of an Angular `LanguageService`.
*
* @experimental
*/
export function createLanguageService(host: LanguageServiceHost): LanguageService {
return new LanguageServiceImpl(host);
}
class LanguageServiceImpl implements LanguageService {
constructor(private host: LanguageServiceHost) {}
private get metadataResolver(): CompileMetadataResolver { return this.host.resolver; }
getTemplateReferences(): string[] { return this.host.getTemplateReferences(); }
getDiagnostics(fileName: string): Diagnostics {
let results: Diagnostics = [];
let templates = this.host.getTemplates(fileName);
if (templates && templates.length) {
results.push(...getTemplateDiagnostics(fileName, this, templates));
}
let declarations = this.host.getDeclarations(fileName);
if (declarations && declarations.length) {
const summary = this.host.getAnalyzedModules();
results.push(...getDeclarationDiagnostics(declarations, summary));
}
return uniqueBySpan(results);
}
getPipesAt(fileName: string, position: number): Pipes {
let templateInfo = this.getTemplateAstAtPosition(fileName, position);
if (templateInfo) {
return templateInfo.pipes.map(
pipeInfo => ({name: pipeInfo.name, symbol: pipeInfo.type.reference}));
}
}
getCompletionsAt(fileName: string, position: number): Completions {
let templateInfo = this.getTemplateAstAtPosition(fileName, position);
if (templateInfo) {
return getTemplateCompletions(templateInfo);
}
}
getDefinitionAt(fileName: string, position: number): Definition {
let templateInfo = this.getTemplateAstAtPosition(fileName, position);
if (templateInfo) {
return getDefinition(templateInfo);
}
}
getHoverAt(fileName: string, position: number): Hover {
let templateInfo = this.getTemplateAstAtPosition(fileName, position);
if (templateInfo) {
return getHover(templateInfo);
}
}
private getTemplateAstAtPosition(fileName: string, position: number): TemplateInfo {
let template = this.host.getTemplateAt(fileName, position);
if (template) {
let astResult = this.getTemplateAst(template, fileName);
if (astResult && astResult.htmlAst && astResult.templateAst)
return {
position,
fileName,
template,
htmlAst: astResult.htmlAst,
directive: astResult.directive,
directives: astResult.directives,
pipes: astResult.pipes,
templateAst: astResult.templateAst,
expressionParser: astResult.expressionParser
};
}
return undefined;
}
getTemplateAst(template: TemplateSource, contextFile: string): AstResult {
let result: AstResult;
try {
const resolvedMetadata =
this.metadataResolver.getNonNormalizedDirectiveMetadata(template.type as any);
const metadata = resolvedMetadata && resolvedMetadata.metadata;
if (metadata) {
const rawHtmlParser = new HtmlParser();
const htmlParser = new I18NHtmlParser(rawHtmlParser);
const expressionParser = new Parser(new Lexer());
const config = new CompilerConfig();
const parser = new TemplateParser(
config, expressionParser, new DomElementSchemaRegistry(), htmlParser, null, []);
const htmlResult = htmlParser.parse(template.source, '');
const analyzedModules = this.host.getAnalyzedModules();
let errors: Diagnostic[] = undefined;
let ngModule = analyzedModules.ngModuleByPipeOrDirective.get(template.type);
if (!ngModule) {
// Reported by the the declaration diagnostics.
ngModule = findSuitableDefaultModule(analyzedModules);
}
if (ngModule) {
const resolvedDirectives = ngModule.transitiveModule.directives.map(
d => this.host.resolver.getNonNormalizedDirectiveMetadata(d.reference));
const directives =
resolvedDirectives.filter(d => d !== null).map(d => d.metadata.toSummary());
const pipes = ngModule.transitiveModule.pipes.map(
p => this.host.resolver.getOrLoadPipeMetadata(p.reference).toSummary());
const schemas = ngModule.schemas;
const parseResult = parser.tryParseHtml(
htmlResult, metadata, template.source, directives, pipes, schemas, '');
result = {
htmlAst: htmlResult.rootNodes,
templateAst: parseResult.templateAst,
directive: metadata, directives, pipes,
parseErrors: parseResult.errors, expressionParser, errors
};
}
}
} catch (e) {
let span = template.span;
if (e.fileName == contextFile) {
span = template.query.getSpanAt(e.line, e.column) || span;
}
result = {errors: [{kind: DiagnosticKind.Error, message: e.message, span}]};
}
return result;
}
}
function uniqueBySpan < T extends {
span: Span;
}
> (elements: T[] | undefined): T[]|undefined {
if (elements) {
const result: T[] = [];
const map = new Map<number, Set<number>>();
for (const element of elements) {
let span = element.span;
let set = map.get(span.start);
if (!set) {
set = new Set();
map.set(span.start, set);
}
if (!set.has(span.end)) {
set.add(span.end);
result.push(element);
}
}
return result;
}
}
function findSuitableDefaultModule(modules: NgAnalyzedModules): CompileNgModuleMetadata {
let result: CompileNgModuleMetadata;
let resultSize = 0;
for (const module of modules.ngModules) {
const moduleSize = module.transitiveModule.directives.length;
if (moduleSize > resultSize) {
result = module;
resultSize = moduleSize;
}
}
return result;
}

View File

@ -0,0 +1,190 @@
/**
* @license
* Copyright Google Inc. 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 {AST, Attribute, BoundDirectivePropertyAst, BoundEventAst, ElementAst, TemplateAst, tokenReference} from '@angular/compiler';
import {TemplateInfo} from './common';
import {getExpressionScope, getExpressionSymbol} from './expressions';
import {HtmlAstPath} from './html_path';
import {TemplateAstPath} from './template_path';
import {Definition, Location, Span, Symbol, SymbolTable} from './types';
import {inSpan, offsetSpan, spanOf} from './utils';
export interface SymbolInfo {
symbol: Symbol;
span: Span;
}
export function locateSymbol(info: TemplateInfo): SymbolInfo {
const templatePosition = info.position - info.template.span.start;
const path = new TemplateAstPath(info.templateAst, templatePosition);
if (path.tail) {
let symbol: Symbol = undefined;
let span: Span = undefined;
const attributeValueSymbol = (ast: AST, inEvent: boolean = false): boolean => {
const attribute = findAttribute(info);
if (attribute) {
if (inSpan(templatePosition, spanOf(attribute.valueSpan))) {
const scope = getExpressionScope(info, path, inEvent);
const expressionOffset = attribute.valueSpan.start.offset + 1;
const result = getExpressionSymbol(
scope, ast, templatePosition - expressionOffset, info.template.query);
if (result) {
symbol = result.symbol;
span = offsetSpan(result.span, expressionOffset);
}
return true;
}
}
return false;
};
path.tail.visit(
{
visitNgContent(ast) {},
visitEmbeddedTemplate(ast) {},
visitElement(ast) {
const component = ast.directives.find(d => d.directive.isComponent);
if (component) {
symbol = info.template.query.getTypeSymbol(component.directive.type.reference);
symbol = symbol && new OverrideKindSymbol(symbol, 'component');
span = spanOf(ast);
} else {
// Find a directive that matches the element name
const directive =
ast.directives.find(d => d.directive.selector.indexOf(ast.name) >= 0);
if (directive) {
symbol = info.template.query.getTypeSymbol(directive.directive.type.reference);
symbol = symbol && new OverrideKindSymbol(symbol, 'directive');
span = spanOf(ast);
}
}
},
visitReference(ast) {
symbol = info.template.query.getTypeSymbol(tokenReference(ast.value));
span = spanOf(ast);
},
visitVariable(ast) {},
visitEvent(ast) {
if (!attributeValueSymbol(ast.handler, /* inEvent */ true)) {
symbol = findOutputBinding(info, path, ast);
symbol = symbol && new OverrideKindSymbol(symbol, 'event');
span = spanOf(ast);
}
},
visitElementProperty(ast) { attributeValueSymbol(ast.value); },
visitAttr(ast) {},
visitBoundText(ast) {
const expressionPosition = templatePosition - ast.sourceSpan.start.offset;
if (inSpan(expressionPosition, ast.value.span)) {
const scope = getExpressionScope(info, path, /* includeEvent */ false);
const result =
getExpressionSymbol(scope, ast.value, expressionPosition, info.template.query);
if (result) {
symbol = result.symbol;
span = offsetSpan(result.span, ast.sourceSpan.start.offset);
}
}
},
visitText(ast) {},
visitDirective(ast) {
symbol = info.template.query.getTypeSymbol(ast.directive.type.reference);
span = spanOf(ast);
},
visitDirectiveProperty(ast) {
if (!attributeValueSymbol(ast.value)) {
symbol = findInputBinding(info, path, ast);
span = spanOf(ast);
}
}
},
null);
if (symbol && span) {
return {symbol, span: offsetSpan(span, info.template.span.start)};
}
}
}
function findAttribute(info: TemplateInfo): Attribute {
const templatePosition = info.position - info.template.span.start;
const path = new HtmlAstPath(info.htmlAst, templatePosition);
return path.first(Attribute);
}
function findInputBinding(
info: TemplateInfo, path: TemplateAstPath, binding: BoundDirectivePropertyAst): Symbol {
const element = path.first(ElementAst);
if (element) {
for (const directive of element.directives) {
const invertedInput = invertMap(directive.directive.inputs);
const fieldName = invertedInput[binding.templateName];
if (fieldName) {
const classSymbol = info.template.query.getTypeSymbol(directive.directive.type.reference);
if (classSymbol) {
return classSymbol.members().get(fieldName);
}
}
}
}
}
function findOutputBinding(
info: TemplateInfo, path: TemplateAstPath, binding: BoundEventAst): Symbol {
const element = path.first(ElementAst);
if (element) {
for (const directive of element.directives) {
const invertedOutputs = invertMap(directive.directive.outputs);
const fieldName = invertedOutputs[binding.name];
if (fieldName) {
const classSymbol = info.template.query.getTypeSymbol(directive.directive.type.reference);
if (classSymbol) {
return classSymbol.members().get(fieldName);
}
}
}
}
}
function invertMap(obj: {[name: string]: string}): {[name: string]: string} {
const result: {[name: string]: string} = {};
for (const name of Object.keys(obj)) {
const v = obj[name];
result[v] = name;
}
return result;
}
/**
* Wrap a symbol and change its kind to component.
*/
class OverrideKindSymbol implements Symbol {
constructor(private sym: Symbol, private kindOverride: string) {}
get name(): string { return this.sym.name; }
get kind(): string { return this.kindOverride; }
get language(): string { return this.sym.language; }
get type(): Symbol|undefined { return this.sym.type; }
get container(): Symbol|undefined { return this.sym.container; }
get public(): boolean { return this.sym.public; }
get callable(): boolean { return this.sym.callable; }
get definition(): Definition { return this.sym.definition; }
members() { return this.sym.members(); }
signatures() { return this.sym.signatures(); }
selectSignature(types: Symbol[]) { return this.sym.selectSignature(types); }
indexed(argument: Symbol) { return this.sym.indexed(argument); }
}

View File

@ -0,0 +1,44 @@
/**
* @license
* Copyright Google Inc. 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 {AngularCompilerOptions, AotCompilerHost, CompilerHost, ModuleResolutionHostAdapter} from '@angular/compiler-cli';
import * as ts from 'typescript';
class ReflectorModuleModuleResolutionHost implements ts.ModuleResolutionHost {
constructor(private host: ts.LanguageServiceHost) {
if (host.directoryExists)
this.directoryExists = directoryName => this.host.directoryExists(directoryName);
}
fileExists(fileName: string): boolean { return !!this.host.getScriptSnapshot(fileName); }
readFile(fileName: string): string {
let snapshot = this.host.getScriptSnapshot(fileName);
if (snapshot) {
return snapshot.getText(0, snapshot.getLength());
}
}
directoryExists: (directoryName: string) => boolean;
}
export class ReflectorHost extends CompilerHost {
constructor(
private getProgram: () => ts.Program, serviceHost: ts.LanguageServiceHost,
options: AngularCompilerOptions) {
super(
null, options,
new ModuleResolutionHostAdapter(new ReflectorModuleModuleResolutionHost(serviceHost)),
{verboseInvalidExpression: true});
}
protected get program() { return this.getProgram(); }
protected set program(value: ts.Program) {
// Discard the result set by ancestor constructor
}
}

View File

@ -0,0 +1,151 @@
/**
* @license
* Copyright Google Inc. 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 {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '@angular/compiler';
import {AstPath} from './ast_path';
import {inSpan, isNarrower, spanOf} from './utils';
export class TemplateAstPath extends AstPath<TemplateAst> {
constructor(ast: TemplateAst[], public position: number, allowWidening: boolean = false) {
super(buildTemplatePath(ast, position, allowWidening));
}
}
function buildTemplatePath(
ast: TemplateAst[], position: number, allowWidening: boolean = false): TemplateAst[] {
const visitor = new TemplateAstPathBuilder(position, allowWidening);
templateVisitAll(visitor, ast);
return visitor.getPath();
}
export class NullTemplateVisitor implements TemplateAstVisitor {
visitNgContent(ast: NgContentAst): void {}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst): void {}
visitElement(ast: ElementAst): void {}
visitReference(ast: ReferenceAst): void {}
visitVariable(ast: VariableAst): void {}
visitEvent(ast: BoundEventAst): void {}
visitElementProperty(ast: BoundElementPropertyAst): void {}
visitAttr(ast: AttrAst): void {}
visitBoundText(ast: BoundTextAst): void {}
visitText(ast: TextAst): void {}
visitDirective(ast: DirectiveAst): void {}
visitDirectiveProperty(ast: BoundDirectivePropertyAst): void {}
}
export class TemplateAstChildVisitor implements TemplateAstVisitor {
constructor(private visitor?: TemplateAstVisitor) {}
// Nodes with children
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
return this.visitChildren(context, visit => {
visit(ast.attrs);
visit(ast.references);
visit(ast.variables);
visit(ast.directives);
visit(ast.providers);
visit(ast.children);
});
}
visitElement(ast: ElementAst, context: any): any {
return this.visitChildren(context, visit => {
visit(ast.attrs);
visit(ast.inputs);
visit(ast.outputs);
visit(ast.references);
visit(ast.directives);
visit(ast.providers);
visit(ast.children);
});
}
visitDirective(ast: DirectiveAst, context: any): any {
return this.visitChildren(context, visit => {
visit(ast.inputs);
visit(ast.hostProperties);
visit(ast.hostEvents);
});
}
// Terminal nodes
visitNgContent(ast: NgContentAst, context: any): any {}
visitReference(ast: ReferenceAst, context: any): any {}
visitVariable(ast: VariableAst, context: any): any {}
visitEvent(ast: BoundEventAst, context: any): any {}
visitElementProperty(ast: BoundElementPropertyAst, context: any): any {}
visitAttr(ast: AttrAst, context: any): any {}
visitBoundText(ast: BoundTextAst, context: any): any {}
visitText(ast: TextAst, context: any): any {}
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any {}
protected visitChildren<T extends TemplateAst>(
context: any,
cb: (visit: (<V extends TemplateAst>(children: V[]|undefined) => void)) => void) {
const visitor = this.visitor || this;
let results: any[][] = [];
function visit<T extends TemplateAst>(children: T[] | undefined) {
if (children && children.length) results.push(templateVisitAll(visitor, children, context));
}
cb(visit);
return [].concat.apply([], results);
}
}
class TemplateAstPathBuilder extends TemplateAstChildVisitor {
private path: TemplateAst[] = [];
constructor(private position: number, private allowWidening: boolean) { super(); }
visit(ast: TemplateAst, context: any): any {
let span = spanOf(ast);
if (inSpan(this.position, span)) {
const len = this.path.length;
if (!len || this.allowWidening || isNarrower(span, spanOf(this.path[len - 1]))) {
this.path.push(ast);
}
} else {
// Returning a value here will result in the children being skipped.
return true;
}
}
visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any {
return this.visitChildren(context, visit => {
// Ignore reference, variable and providers
visit(ast.attrs);
visit(ast.directives);
visit(ast.children);
});
}
visitElement(ast: ElementAst, context: any): any {
return this.visitChildren(context, visit => {
// Ingnore providers
visit(ast.attrs);
visit(ast.inputs);
visit(ast.outputs);
visit(ast.references);
visit(ast.directives);
visit(ast.children);
});
}
visitDirective(ast: DirectiveAst, context: any): any {
// Ignore the host properties of a directive
const result = this.visitChildren(context, visit => { visit(ast.inputs); });
// We never care about the diretive itself, just its inputs.
if (this.path[this.path.length - 1] == ast) {
this.path.pop();
}
return result;
}
getPath() { return this.path; }
}

View File

@ -0,0 +1,139 @@
/**
* @license
* Copyright Google Inc. 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 * as ts from 'typescript';
import {createLanguageService} from './language_service';
import {Completion, Diagnostic, LanguageService, LanguageServiceHost} from './types';
import {TypeScriptServiceHost} from './typescript_host';
export function create(info: any /* ts.server.PluginCreateInfo */): ts.LanguageService {
// Create the proxy
const proxy: ts.LanguageService = Object.create(null);
const oldLS: ts.LanguageService = info.languageService;
for (const k in oldLS) {
(<any>proxy)[k] = function() { return (oldLS as any)[k].apply(oldLS, arguments); };
}
function completionToEntry(c: Completion): ts.CompletionEntry {
return {kind: c.kind, name: c.name, sortText: c.sort, kindModifiers: ''};
}
function diagnosticToDiagnostic(d: Diagnostic, file: ts.SourceFile): ts.Diagnostic {
return {
file,
start: d.span.start,
length: d.span.end - d.span.start,
messageText: d.message,
category: ts.DiagnosticCategory.Error,
code: 0
};
}
function tryOperation(attempting: string, callback: () => void) {
try {
callback();
} catch (e) {
info.project.projectService.logger.info(`Failed to ${attempting}: ${e.toString()}`);
info.project.projectService.logger.info(`Stack trace: ${e.stack}`);
}
}
const serviceHost = new TypeScriptServiceHost(info.languageServiceHost, info.languageService);
const ls = createLanguageService(serviceHost);
serviceHost.setSite(ls);
proxy.getCompletionsAtPosition = function(fileName: string, position: number) {
let base = oldLS.getCompletionsAtPosition(fileName, position);
tryOperation('get completions', () => {
const results = ls.getCompletionsAt(fileName, position);
if (results && results.length) {
if (base === undefined) {
base = {
isGlobalCompletion: false,
isMemberCompletion: false,
isNewIdentifierLocation: false,
entries: []
};
}
for (const entry of results) {
base.entries.push(completionToEntry(entry));
}
}
});
return base;
};
proxy.getQuickInfoAtPosition = function(fileName: string, position: number): ts.QuickInfo {
let base = oldLS.getQuickInfoAtPosition(fileName, position);
tryOperation('get quick info', () => {
const ours = ls.getHoverAt(fileName, position);
if (ours) {
const displayParts: typeof base.displayParts = [];
for (const part of ours.text) {
displayParts.push({kind: part.language, text: part.text});
}
base = {
displayParts,
documentation: [],
kind: 'angular',
kindModifiers: 'what does this do?',
textSpan: {start: ours.span.start, length: ours.span.end - ours.span.start},
tags: [],
};
}
});
return base;
};
proxy.getSemanticDiagnostics = function(fileName: string) {
let base = oldLS.getSemanticDiagnostics(fileName);
if (base === undefined) {
base = [];
}
tryOperation('get diagnostics', () => {
info.project.projectService.logger.info(`Computing Angular semantic diagnostics...`);
const ours = ls.getDiagnostics(fileName);
if (ours && ours.length) {
const file = oldLS.getProgram().getSourceFile(fileName);
base.push.apply(base, ours.map(d => diagnosticToDiagnostic(d, file)));
}
});
return base;
};
proxy.getDefinitionAtPosition = function(
fileName: string, position: number): ts.DefinitionInfo[] {
let base = oldLS.getDefinitionAtPosition(fileName, position);
if (base && base.length) {
return base;
}
tryOperation('get definition', () => {
const ours = ls.getDefinitionAt(fileName, position);
if (ours && ours.length) {
base = base || [];
for (const loc of ours) {
base.push({
fileName: loc.fileName,
textSpan: {start: loc.span.start, length: loc.span.end - loc.span.start},
name: '',
kind: 'definition',
containerName: loc.fileName,
containerKind: 'file'
});
}
}
});
return base;
};
return proxy;
}

View File

@ -0,0 +1,728 @@
/**
* @license
* Copyright Google Inc. 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 {CompileDirectiveMetadata, CompileMetadataResolver, NgAnalyzedModules, StaticSymbol} from '@angular/compiler';
/**
* The range of a span of text in a source file.
*
* @experimental
*/
export interface Span {
/**
* The first code-point of the span as an offset relative to the beginning of the source assuming
* a UTF-16 encoding.
*/
start: number;
/**
* The first code-point after the span as an offset relative to the beginning of the source
* assuming a UTF-16 encoding.
*/
end: number;
}
/**
* The information `LanguageService` needs from the `LanguageServiceHost` to describe the content of
* a template and the
* langauge context the template is in.
*
* A host interface; see `LanguageSeriviceHost`.
*
* @experimental
*/
export interface TemplateSource {
/**
* The source of the template.
*/
readonly source: string;
/**
* The version of the source. As files are modified the version should change. That is, if the
* `LanguageService` requesting
* template infomration for a source file and that file has changed since the last time the host
* was asked for the file then
* this version string should be different. No assumptions are made about the format of this
* string.
*
* The version can change more often than the source but should not change less often.
*/
readonly version: string;
/**
* The span of the template within the source file.
*/
readonly span: Span;
/**
* A static symbol for the template's component.
*/
readonly type: StaticSymbol;
/**
* The `SymbolTable` for the members of the component.
*/
readonly members: SymbolTable;
/**
* A `SymbolQuery` for the context of the template.
*/
readonly query: SymbolQuery;
}
/**
* A sequence of template sources.
*
* A host type; see `LanguageSeriviceHost`.
*
* @experimental
*/
export type TemplateSources = TemplateSource[] /* | undefined */;
/**
* Error information found getting declaration information
*
* A host type; see `LanagueServiceHost`.
*
* @experimental
*/
export interface DeclarationError {
/**
* The span of the error in the declaration's module.
*/
readonly span: Span;
/**
* The message to display describing the error.
*/
readonly message: string;
}
/**
* Information about the component declarations.
*
* A file might contain a declaration without a template because the file contains only
* templateUrl references. However, the compoennt declaration might contain errors that
* need to be reported such as the template string is missing or the component is not
* declared in a module. These error should be reported on the declaration, not the
* template.
*
* A host type; see `LanguageSeriviceHost`.
*
* @experimental
*/
export interface Declaration {
/**
* The static symbol of the compponent being declared.
*/
readonly type: StaticSymbol;
/**
* The span of the declaration annotation reference (e.g. the 'Component' or 'Directive'
* reference).
*/
readonly declarationSpan: Span;
/**
* Reference to the compiler directive metadata for the declaration.
*/
readonly metadata?: CompileDirectiveMetadata;
/**
* Error reported trying to get the metadata.
*/
readonly errors: DeclarationError[];
}
/**
* A sequence of declarations.
*
* A host type; see `LanguageSeriviceHost`.
*
* @experimental
*/
export type Declarations = Declaration[];
/**
* An enumeration of basic types.
*
* A `LanguageServiceHost` interface.
*
* @experimental
*/
export enum BuiltinType {
/**
* The type is a type that can hold any other type.
*/
Any,
/**
* The type of a string literal.
*/
String,
/**
* The type of a numeric literal.
*/
Number,
/**
* The type of the `true` and `false` literals.
*/
Boolean,
/**
* The type of the `undefined` literal.
*/
Undefined,
/**
* the type of the `null` literal.
*/
Null,
/**
* the type is an unbound type parameter.
*/
Unbound,
/**
* Not a built-in type.
*/
Other
}
/**
* A symbol describing a language element that can be referenced by expressions
* in an Angular template.
*
* A `LanguageServiceHost` interface.
*
* @experimental
*/
export interface Symbol {
/**
* The name of the symbol as it would be referenced in an Angular expression.
*/
readonly name: string;
/**
* The kind of completion this symbol should generate if included.
*/
readonly kind: string;
/**
* The language of the source that defines the symbol. (e.g. typescript for TypeScript,
* ng-template for an Angular template, etc.)
*/
readonly language: string;
/**
* A symbol representing type of the symbol.
*/
readonly type: Symbol /* | undefined */;
/**
* A symbol for the container of this symbol. For example, if this is a method, the container
* is the class or interface of the method. If no container is appropriate, undefined is
* returned.
*/
readonly container: Symbol /* | undefined */;
/**
* The symbol is public in the container.
*/
readonly public: boolean;
/**
* `true` if the symbol can be the target of a call.
*/
readonly callable: boolean;
/**
* The location of the definition of the symbol
*/
readonly definition: Definition;
/**
* A table of the members of the symbol; that is, the members that can appear
* after a `.` in an Angular expression.
*
*/
members(): SymbolTable;
/**
* The list of overloaded signatures that can be used if the symbol is the
* target of a call.
*/
signatures(): Signature[];
/**
* Return which signature of returned by `signatures()` would be used selected
* given the `types` supplied. If no signature would match, this method should
* return `undefined`.
*/
selectSignature(types: Symbol[]): Signature /* | undefined */;
/**
* Return the type of the expression if this symbol is indexed by `argument`.
* If the symbol cannot be indexed, this method should return `undefined`.
*/
indexed(argument: Symbol): Symbol /* | undefined */;
}
/**
* A table of `Symbol`s accessible by name.
*
* A `LanguageServiceHost` interface.
*
* @experimental
*/
export interface SymbolTable {
/**
* The number of symbols in the table.
*/
readonly size: number;
/**
* Get the symbol corresponding to `key` or `undefined` if there is no symbol in the
* table by the name `key`.
*/
get(key: string): Symbol /* | undefined */;
/**
* Returns `true` if the table contains a `Symbol` with the name `key`.
*/
has(key: string): boolean;
/**
* Returns all the `Symbol`s in the table. The order should be, but is not required to be,
* in declaration order.
*/
values(): Symbol[];
}
/**
* A description of a function or method signature.
*
* A `LanguageServiceHost` interface.
*
* @experimental
*/
export interface Signature {
/**
* The arguments of the signture. The order of `argumetnts.symbols()` must be in the order
* of argument declaration.
*/
readonly arguments: SymbolTable;
/**
* The symbol of the signature result type.
*/
readonly result: Symbol;
}
/**
* Describes the language context in which an Angular expression is evaluated.
*
* A `LanguageServiceHost` interface.
*
* @experimental
*/
export interface SymbolQuery {
/**
* Return the built-in type this symbol represents or Other if it is not a built-in type.
*/
getTypeKind(symbol: Symbol): BuiltinType;
/**
* Return a symbol representing the given built-in type.
*/
getBuiltinType(kind: BuiltinType): Symbol;
/**
* Return the symbol for a type that represents the union of all the types given. Any value
* of one of the types given should be assignable to the returned type. If no one type can
* be constructed then this should be the Any type.
*/
getTypeUnion(...types: Symbol[]): Symbol;
/**
* Return a symbol for an array type that has the `type` as its element type.
*/
getArrayType(type: Symbol): Symbol;
/**
* Return element type symbol for an array type if the `type` is an array type. Otherwise return
* undefined.
*/
getElementType(type: Symbol): Symbol /* | undefined */;
/**
* Return a type that is the non-nullable version of the given type. If `type` is already
* non-nullable, return `type`.
*/
getNonNullableType(type: Symbol): Symbol;
/**
* Return a symbol table for the pipes that are in scope.
*/
getPipes(): SymbolTable;
/**
* Return the type symbol for the given static symbol.
*/
getTypeSymbol(type: StaticSymbol): Symbol;
/**
* Return the members that are in the context of a type's template reference.
*/
getTemplateContext(type: StaticSymbol): SymbolTable;
/**
* Produce a symbol table with the given symbols. Used to produce a symbol table
* for use with mergeSymbolTables().
*/
createSymbolTable(symbols: SymbolDeclaration[]): SymbolTable;
/**
* Produce a merged symbol table. If the symbol tables contain duplicate entries
* the entries of the latter symbol tables will obscure the entries in the prior
* symbol tables.
*
* The symbol tables passed to this routine MUST be produces by the same instance
* of SymbolQuery that is being called.
*/
mergeSymbolTable(symbolTables: SymbolTable[]): SymbolTable;
/**
* Return the span of the narrowest non-token node at the given location.
*/
getSpanAt(line: number, column: number): Span /* | undefined */;
}
/**
* The host for a `LanguageService`. This provides all the `LanguageService` requires to respond to
* the `LanguageService` requests.
*
* This interface describes the requirements of the `LanguageService` on its host.
*
* The host interface is host language agnostic.
*
* Adding optional member to this interface or any interface that is described as a
* `LanguageServiceHost`
* interface is not considered a breaking change as defined by SemVer. Removing a method or changing
* a
* member from required to optional will also not be considered a breaking change.
*
* If a member is deprecated it will be changed to optional in a minor release before it is removed
* in
* a major release.
*
* Adding a required member or changing a method's parameters, is considered a breaking change and
* will
* only be done when breaking changes are allowed. When possible, a new optional member will be
* added and
* the old member will be deprecated. The new member will then be made required in and the old
* member will
* be removed only when breaking chnages are allowed.
*
* While an interface is marked as experimental breaking-changes will be allowed between minor
* releases.
* After an interface is marked as stable breaking-changes will only be allowed between major
* releases.
* No breaking changes are allowed between patch releases.
*
* @experimental
*/
export interface LanguageServiceHost {
/**
* The resolver to use to find compiler metadata.
*/
readonly resolver: CompileMetadataResolver;
/**
* Returns the template information for templates in `fileName` at the given location. If
* `fileName`
* refers to a template file then the `position` should be ignored. If the `position` is not in a
* template literal string then this method should return `undefined`.
*/
getTemplateAt(fileName: string, position: number): TemplateSource /* |undefined */;
/**
* Return the template source information for all templates in `fileName` or for `fileName` if it
* is
* a template file.
*/
getTemplates(fileName: string): TemplateSources;
/**
* Returns the Angular declarations in the given file.
*/
getDeclarations(fileName: string): Declarations;
/**
* Return a summary of all Angular modules in the project.
*/
getAnalyzedModules(): NgAnalyzedModules;
/**
* Return a list all the template files referenced by the project.
*/
getTemplateReferences(): string[];
}
/**
* The kinds of completions generated by the language service.
*
* A 'LanguageService' interface.
*
* @experimental
*/
export type CompletionKind = 'attribute' | 'html attribute' | 'component' | 'element' | 'entity' |
'key' | 'method' | 'pipe' | 'property' | 'type' | 'reference' | 'variable';
/**
* An item of the completion result to be displayed by an editor.
*
* A `LanguageService` interface.
*
* @experimental
*/
export interface Completion {
/**
* The kind of comletion.
*/
kind: CompletionKind;
/**
* The name of the completion to be displayed
*/
name: string;
/**
* The key to use to sort the completions for display.
*/
sort: string;
}
/**
* A sequence of completions.
*
* @experimental
*/
export type Completions = Completion[] /* | undefined */;
/**
* A file and span.
*/
export interface Location {
fileName: string;
span: Span;
}
/**
* A defnition location(s).
*/
export type Definition = Location[] /* | undefined */;
/**
* The kind of diagnostic message.
*
* @experimental
*/
export enum DiagnosticKind {
Error,
Warning,
}
/**
* An template diagnostic message to display.
*
* @experimental
*/
export interface Diagnostic {
/**
* The kind of diagnostic message
*/
kind: DiagnosticKind;
/**
* The source span that should be highlighted.
*/
span: Span;
/**
* The text of the diagnostic message to display.
*/
message: string;
}
/**
* A sequence of diagnostic message.
*
* @experimental
*/
export type Diagnostics = Diagnostic[];
/**
* Information about the pipes that are available for use in a template.
*
* A `LanguageService` interface.
*
* @experimental
*/
export interface PipeInfo {
/**
* The name of the pipe.
*/
name: string;
/**
* The static symbol for the pipe's constructor.
*/
symbol: StaticSymbol;
}
/**
* A sequence of pipe information.
*
* @experimental
*/
export type Pipes = PipeInfo[] /* | undefined */;
/**
* Describes a symbol to type binding used to build a symbol table.
*
* A `LanguageServiceHost` interface.
*
* @experimental
*/
export interface SymbolDeclaration {
/**
* The name of the symbol in table.
*/
readonly name: string;
/**
* The kind of symbol to declare.
*/
readonly kind: CompletionKind;
/**
* Type of the symbol. The type symbol should refer to a symbol for a type.
*/
readonly type: Symbol;
/**
* The definion of the symbol if one exists.
*/
readonly definition?: Definition;
}
/**
* A section of hover text. If the text is code then langauge should be provided.
* Otherwise the text is assumed to be Markdown text that will be sanitized.
*/
export interface HoverTextSection {
/**
* Source code or markdown text describing the symbol a the hover location.
*/
readonly text: string;
/**
* The langauge of the source if `text` is a souce code fragment.
*/
readonly language?: string;
}
/**
* Hover infomration for a symbol at the hover location.
*/
export interface Hover {
/**
* The hover text to display for the symbol at the hover location. If the text includes
* source code, the section will specify which langauge it should be interpreted as.
*/
readonly text: HoverTextSection[];
/**
* The span of source the hover covers.
*/
readonly span: Span;
}
/**
* An instance of an Angular language service created by `createLanguageService()`.
*
* The language service returns information about Angular templates that are included in a project
* as
* defined by the `LanguageServiceHost`.
*
* When a method expects a `fileName` this file can either be source file in the project that
* contains
* a template in a string literal or a template file referenced by the project returned by
* `getTemplateReference()`. All other files will cause the method to return `undefined`.
*
* If a method takes a `position`, it is the offset of the UTF-16 code-point relative to the
* beginning
* of the file reference by `fileName`.
*
* This interface and all interfaces and types marked as `LanguageService` types, describe a
* particlar
* implementation of the Angular language service and is not intented to be implemented. Adding
* members
* to the interface will not be considered a breaking change as defined by SemVer.
*
* Removing a member or making a member optional, changing a method parameters, or changing a
* member's
* type will all be considered a breaking change.
*
* While an interface is marked as experimental breaking-changes will be allowed between minor
* releases.
* After an interface is marked as stable breaking-changes will only be allowed between major
* releases.
* No breaking changes are allowed between patch releases.
*
* @experimental
*/
export interface LanguageService {
/**
* Returns a list of all the external templates referenced by the project.
*/
getTemplateReferences(): string[] /* | undefined */;
/**
* Returns a list of all error for all templates in the given file.
*/
getDiagnostics(fileName: string): Diagnostics /* | undefined */;
/**
* Return the completions at the given position.
*/
getCompletionsAt(fileName: string, position: number): Completions /* | undefined */;
/**
* Return the definition location for the symbol at position.
*/
getDefinitionAt(fileName: string, position: number): Definition /* | undefined */;
/**
* Return the hover information for the symbol at position.
*/
getHoverAt(fileName: string, position: number): Hover /* | undefined */;
/**
* Return the pipes that are available at the given position.
*/
getPipesAt(fileName: string, position: number): Pipes /* | undefined */;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,98 @@
/**
* @license
* Copyright Google Inc. 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 {CompileDirectiveSummary, CompileTypeMetadata, CssSelector, ParseSourceSpan, SelectorMatcher, identifierName} from '@angular/compiler';
import {SelectorInfo, TemplateInfo} from './common';
import {Span} from './types';
export interface SpanHolder {
sourceSpan: ParseSourceSpan;
endSourceSpan?: ParseSourceSpan;
children?: SpanHolder[];
}
export function isParseSourceSpan(value: any): value is ParseSourceSpan {
return value && !!value.start;
}
export function spanOf(span?: SpanHolder | ParseSourceSpan): Span {
if (!span) return undefined;
if (isParseSourceSpan(span)) {
return {start: span.start.offset, end: span.end.offset};
} else {
if (span.endSourceSpan) {
return {start: span.sourceSpan.start.offset, end: span.endSourceSpan.end.offset};
} else if (span.children && span.children.length) {
return {
start: span.sourceSpan.start.offset,
end: spanOf(span.children[span.children.length - 1]).end
};
}
return {start: span.sourceSpan.start.offset, end: span.sourceSpan.end.offset};
}
}
export function inSpan(position: number, span?: Span, exclusive?: boolean): boolean {
return span && exclusive ? position >= span.start && position < span.end :
position >= span.start && position <= span.end;
}
export function offsetSpan(span: Span, amount: number): Span {
return {start: span.start + amount, end: span.end + amount};
}
export function isNarrower(spanA: Span, spanB: Span): boolean {
return spanA.start >= spanB.start && spanA.end <= spanB.end;
}
export function hasTemplateReference(type: CompileTypeMetadata): boolean {
if (type.diDeps) {
for (let diDep of type.diDeps) {
if (diDep.token.identifier && identifierName(diDep.token.identifier) == 'TemplateRef')
return true;
}
}
return false;
}
export function getSelectors(info: TemplateInfo): SelectorInfo {
const map = new Map<CssSelector, CompileDirectiveSummary>();
const selectors = flatten(info.directives.map(directive => {
const selectors = CssSelector.parse(directive.selector);
selectors.forEach(selector => map.set(selector, directive));
return selectors;
}));
return {selectors, map};
}
export function flatten<T>(a: T[][]) {
return (<T[]>[]).concat(...a);
}
export function removeSuffix(value: string, suffix: string) {
if (value.endsWith(suffix)) return value.substring(0, value.length - suffix.length);
return value;
}
export function uniqueByName < T extends {
name: string;
}
> (elements: T[] | undefined): T[]|undefined {
if (elements) {
const result: T[] = [];
const set = new Set<string>();
for (const element of elements) {
if (!set.has(element.name)) {
set.add(element.name);
result.push(element);
}
}
return result;
}
}

View File

@ -0,0 +1,19 @@
/**
* @license
* Copyright Google Inc. 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
*/
/**
* @module
* @description
* Entry point for all public APIs of the common package.
*/
import {Version} from '@angular/core';
/**
* @stable
*/
export const VERSION = new Version('0.0.0-PLACEHOLDER');