feat(language-service): add services to support editors (#12987)

This commit is contained in:
Chuck Jazdzewski 2016-11-22 09:10:23 -08:00 committed by GitHub
parent ef96763fa4
commit 519a324454
37 changed files with 6688 additions and 9 deletions

View File

@ -17,6 +17,7 @@ PACKAGES=(core
upgrade
router
compiler-cli
language-service
benchpress)
BUILD_ALL=true
BUNDLE=true
@ -184,7 +185,6 @@ do
mv ${UMD_ES5_PATH}.tmp ${UMD_ES5_PATH}
$UGLIFYJS -c --screw-ie8 --comments -o ${UMD_ES5_MIN_PATH} ${UMD_ES5_PATH}
if [[ -e rollup-testing.config.js ]]; then
echo "====== Rollup ${PACKAGE} testing"
../../../node_modules/.bin/rollup -c rollup-testing.config.js

View File

@ -52,6 +52,7 @@ module.exports = function(config) {
'dist/all/@angular/compiler-cli/**',
'dist/all/@angular/compiler/test/aot/**',
'dist/all/@angular/benchpress/**',
'dist/all/@angular/language-service/**',
'dist/all/angular1_router.js',
'dist/all/@angular/platform-browser/testing/e2e_util.js',
'dist/examples/**/e2e_test/**',

View File

@ -0,0 +1,22 @@
/**
* @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 language service package.
*/
import * as ts from 'typescript';
import {LanguageServicePlugin} from './src/ts_plugin';
export {createLanguageService} from './src/language_service';
export {Completion, Completions, Declaration, Declarations, Definition, Diagnostic, Diagnostics, Hover, HoverTextSection, LanguageService, LanguageServiceHost, Location, Span, TemplateSource, TemplateSources} from './src/types';
export {TypeScriptServiceHost, createLanguageServiceFromTypescript} from './src/typescript_host';
export default LanguageServicePlugin;

View File

@ -0,0 +1,14 @@
{
"name": "@angular/language-service",
"version": "0.0.0-PLACEHOLDER",
"description": "Angular 2 - language services",
"main": "bundles/language-service.umd.js",
"module": "index.js",
"typings": "index.d.ts",
"author": "angular",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/angular/angular.git"
}
}

View File

@ -0,0 +1,81 @@
/**
* @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 commonjs from 'rollup-plugin-commonjs';
import * as path from 'path';
var m = /^\@angular\/((\w|\-)+)(\/(\w|\d|\/|\-)+)?$/;
var location = normalize('../../../dist/packages-dist') + '/';
var rxjsLocation = normalize('../../../node_modules/rxjs');
var esm = 'esm/';
var locations = {
'tsc-wrapped': normalize('../../../dist/tools/@angular') + '/',
};
var esm_suffixes = {};
function normalize(fileName) {
return path.resolve(__dirname, fileName);
}
function resolve(id, from) {
// console.log('Resolve id:', id, 'from', from)
if (id == '@angular/tsc-wrapped') {
// Hack to restrict the import to not include the index of @angular/tsc-wrapped so we don't
// rollup tsickle.
return locations['tsc-wrapped'] + 'tsc-wrapped/src/collector.js';
}
var match = m.exec(id);
if (match) {
var packageName = match[1];
var esm_suffix = esm_suffixes[packageName] || '';
var loc = locations[packageName] || location;
var r = loc + esm_suffix + packageName + (match[3] || '/index') + '.js';
// console.log('** ANGULAR MAPPED **: ', r);
return r;
}
if (id && id.startsWith('rxjs/')) {
const resolved = `${rxjsLocation}${id.replace('rxjs', '')}.js`;
return resolved;
}
}
var banner = `
var $deferred, $resolved, $provided;
function $getModule(name) { return $provided[name] || require(name); }
function define(modules, cb) { $deferred = { modules: modules, cb: cb }; }
module.exports = function(provided) {
if ($resolved) return $resolved;
var result = {};
$provided = Object.assign({}, provided || {}, { exports: result });
$deferred.cb.apply(this, $deferred.modules.map($getModule));
$resolved = result;
return result;
}
`;
export default {
entry: '../../../dist/packages-dist/language-service/index.js',
dest: '../../../dist/packages-dist/language-service/bundles/language-service.umd.js',
format: 'amd',
moduleName: 'ng.language_service',
exports: 'named',
external: [
'fs',
'path',
'typescript',
],
globals: {
'typescript': 'ts',
'path': 'path',
'fs': 'fs',
},
banner: banner,
plugins: [{resolveId: resolve}, commonjs()]
}

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,53 @@
/**
* @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} from '@angular/compiler';
import {Parser} from '@angular/compiler/src/expression_parser/parser';
import {Node as HtmlAst} from '@angular/compiler/src/ml_parser/ast';
import {ParseError} from '@angular/compiler/src/parse_util';
import {CssSelector} from '@angular/compiler/src/selector';
import {TemplateAst} from '@angular/compiler/src/template_parser/template_ast';
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,495 @@
/**
* @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, ImplicitReceiver, ParseSpan, PropertyRead} from '@angular/compiler/src/expression_parser/ast';
import {Attribute, Element, Node as HtmlAst, Text} from '@angular/compiler/src/ml_parser/ast';
import {getHtmlTagDefinition} from '@angular/compiler/src/ml_parser/html_tags';
import {NAMED_ENTITIES, TagContentType, splitNsName} from '@angular/compiler/src/ml_parser/tags';
import {CssSelector, SelectorMatcher} from '@angular/compiler/src/selector';
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '@angular/compiler/src/template_parser/template_ast';
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,250 @@
/**
* @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, StaticSymbol} from '@angular/compiler';
import {NgAnalyzedModules} from '@angular/compiler/src/aot/compiler';
import {AST} from '@angular/compiler/src/expression_parser/ast';
import {Attribute} from '@angular/compiler/src/ml_parser/ast';
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '@angular/compiler/src/template_parser/template_ast';
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) {
let report = (message: string) => {
results.push(
<Diagnostic>{kind: DiagnosticKind.Error, span: declaration.declarationSpan, message});
};
if (declaration.error) {
report(declaration.error);
}
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)
.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,770 @@
/**
* @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 {StaticSymbol} from '@angular/compiler';
import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '@angular/compiler/src/expression_parser/ast';
import {ElementAst, EmbeddedTemplateAst, ReferenceAst, TemplateAst, templateVisitAll} from '@angular/compiler/src/template_parser/template_ast';
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 function getExpressionDiagnostics(
scope: SymbolTable, ast: AST, query: SymbolQuery): TypeDiagnostic[] {
const analyzer = new AstType(scope, query);
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) {}
getType(ast: AST): Symbol { return ast.visit(this); }
getDiagnostics(ast: AST): TypeDiagnostic[] {
this.diagnostics = [];
ast.visit(this);
return this.diagnostics;
}
visitBinary(ast: Binary): Symbol {
// Treat undefined and null as other.
function normalize(kind: BuiltinType): BuiltinType {
switch (kind) {
case BuiltinType.Undefined:
case BuiltinType.Null:
return BuiltinType.Other;
}
return kind;
}
const leftType = this.getType(ast.left);
const rightType = this.getType(ast.right);
const leftKind = normalize(this.query.getTypeKind(leftType));
const rightKind = normalize(this.query.getTypeKind(rightType));
// The following swtich implements operator typing similar to the
// type production tables in the TypeScript specification.
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);
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(reference.value.reference);
}
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;
if (info.template.query.getTypeKind(type) === BuiltinType.Any) {
// 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 => d.directive.type.name == 'NgFor');
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/src/ml_parser/ast';
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,186 @@
/**
* @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 {NgAnalyzedModules} from '@angular/compiler/src/aot/compiler';
import {CompileNgModuleMetadata} from '@angular/compiler/src/compile_metadata';
import {Lexer} from '@angular/compiler/src/expression_parser/lexer';
import {Parser} from '@angular/compiler/src/expression_parser/parser';
import {I18NHtmlParser} from '@angular/compiler/src/i18n/i18n_html_parser';
import {CompileMetadataResolver} from '@angular/compiler/src/metadata_resolver';
import {HtmlParser} from '@angular/compiler/src/ml_parser/html_parser';
import {DomElementSchemaRegistry} from '@angular/compiler/src/schema/dom_element_schema_registry';
import {TemplateParser} from '@angular/compiler/src/template_parser/template_parser';
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 directive =
this.metadataResolver.getNonNormalizedDirectiveMetadata(template.type as any);
if (directive) {
const rawHtmlParser = new HtmlParser();
const htmlParser = new I18NHtmlParser(rawHtmlParser);
const expressionParser = new Parser(new Lexer());
const parser = new TemplateParser(
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 directives = ngModule.transitiveModule.directives.map(
d => this.host.resolver.getNonNormalizedDirectiveMetadata(d.reference).toSummary());
const pipes = ngModule.transitiveModule.pipes.map(
p => this.host.resolver.getOrLoadPipeMetadata(p.reference).toSummary());
const schemas = ngModule.schemas;
const parseResult = parser.tryParseHtml(
htmlResult, directive, template.source, directives, pipes, schemas, '');
result = {
htmlAst: htmlResult.rootNodes,
templateAst: parseResult.templateAst, directive, 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,192 @@
/**
* @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} from '@angular/compiler/src/expression_parser/ast';
import {Attribute} from '@angular/compiler/src/ml_parser/ast';
import {BoundDirectivePropertyAst, BoundEventAst, ElementAst, TemplateAst} from '@angular/compiler/src/template_parser/template_ast';
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(ast.value.reference);
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,158 @@
/**
* @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 {StaticReflectorHost, StaticSymbol} from '@angular/compiler';
import {MetadataCollector} from '@angular/tsc-wrapped/src/collector';
import {ModuleMetadata} from '@angular/tsc-wrapped/src/schema';
import * as path from 'path';
import * as ts from 'typescript';
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
const DTS = /\.d\.ts$/;
let serialNumber = 0;
class ReflectorModuleModuleResolutionHost implements ts.ModuleResolutionHost {
private forceExists: string[] = [];
constructor(private host: ts.LanguageServiceHost) {
if (host.directoryExists)
this.directoryExists = directoryName => this.host.directoryExists(directoryName);
}
fileExists(fileName: string): boolean {
return !!this.host.getScriptSnapshot(fileName) || this.forceExists.indexOf(fileName) >= 0;
}
readFile(fileName: string): string {
let snapshot = this.host.getScriptSnapshot(fileName);
if (snapshot) {
return snapshot.getText(0, snapshot.getLength());
}
}
directoryExists: (directoryName: string) => boolean;
forceExist(fileName: string): void { this.forceExists.push(fileName); }
}
export class ReflectorHost implements StaticReflectorHost {
private metadataCollector: MetadataCollector;
private moduleResolverHost: ReflectorModuleModuleResolutionHost;
private _typeChecker: ts.TypeChecker;
private metadataCache = new Map<string, MetadataCacheEntry>();
constructor(
private getProgram: () => ts.Program, private serviceHost: ts.LanguageServiceHost,
private options: ts.CompilerOptions, private basePath: string) {
this.moduleResolverHost = new ReflectorModuleModuleResolutionHost(serviceHost);
this.metadataCollector = new MetadataCollector();
}
getCanonicalFileName(fileName: string): string { return fileName; }
private get program() { return this.getProgram(); }
public moduleNameToFileName(moduleName: string, containingFile: string) {
if (!containingFile || !containingFile.length) {
if (moduleName.indexOf('.') === 0) {
throw new Error('Resolution of relative paths requires a containing file.');
}
// Any containing file gives the same result for absolute imports
containingFile = this.getCanonicalFileName(path.join(this.basePath, 'index.ts'));
}
moduleName = moduleName.replace(EXT, '');
const resolved =
ts.resolveModuleName(moduleName, containingFile, this.options, this.moduleResolverHost)
.resolvedModule;
return resolved ? resolved.resolvedFileName : null;
};
/**
* We want a moduleId that will appear in import statements in the generated code.
* These need to be in a form that system.js can load, so absolute file paths don't work.
* Relativize the paths by checking candidate prefixes of the absolute path, to see if
* they are resolvable by the moduleResolution strategy from the CompilerHost.
*/
fileNameToModuleName(importedFile: string, containingFile: string) {
// TODO(tbosch): if a file does not yet exist (because we compile it later),
// we still need to create it so that the `resolve` method works!
if (!this.moduleResolverHost.fileExists(importedFile)) {
this.moduleResolverHost.forceExist(importedFile);
}
const parts = importedFile.replace(EXT, '').split(path.sep).filter(p => !!p);
for (let index = parts.length - 1; index >= 0; index--) {
let candidate = parts.slice(index, parts.length).join(path.sep);
if (this.moduleNameToFileName('.' + path.sep + candidate, containingFile) === importedFile) {
return `./${candidate}`;
}
if (this.moduleNameToFileName(candidate, containingFile) === importedFile) {
return candidate;
}
}
throw new Error(
`Unable to find any resolvable import for ${importedFile} relative to ${containingFile}`);
}
private get typeChecker(): ts.TypeChecker {
let result = this._typeChecker;
if (!result) {
result = this._typeChecker = this.program.getTypeChecker();
}
return result;
}
private typeCache = new Map<string, StaticSymbol>();
// TODO(alexeagle): take a statictype
getMetadataFor(filePath: string): ModuleMetadata[] {
if (!this.moduleResolverHost.fileExists(filePath)) {
throw new Error(`No such file '${filePath}'`);
}
if (DTS.test(filePath)) {
const metadataPath = filePath.replace(DTS, '.metadata.json');
if (this.moduleResolverHost.fileExists(metadataPath)) {
return this.readMetadata(metadataPath);
}
}
let sf = this.program.getSourceFile(filePath);
if (!sf) {
throw new Error(`Source file ${filePath} not present in program.`);
}
const entry = this.metadataCache.get(sf.path);
const version = this.serviceHost.getScriptVersion(sf.path);
if (entry && entry.version == version) {
if (!entry.content) return undefined;
return [entry.content];
}
const metadata = this.metadataCollector.getMetadata(sf);
this.metadataCache.set(sf.path, {version, content: metadata});
if (metadata) return [metadata];
}
readMetadata(filePath: string) {
try {
const text = this.moduleResolverHost.readFile(filePath);
const result = JSON.parse(text);
if (!Array.isArray(result)) return [result];
return result;
} catch (e) {
console.error(`Failed to read JSON file ${filePath}`);
throw e;
}
}
}
interface MetadataCacheEntry {
version: string;
content: ModuleMetadata;
}

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/src/template_parser/template_ast';
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,77 @@
/**
* @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 {LanguageService, LanguageServiceHost} from './types';
import {TypeScriptServiceHost} from './typescript_host';
/** A plugin to TypeScript's langauge service that provide language services for
* templates in string literals.
*
* @experimental
*/
export class LanguageServicePlugin {
private ts: typeof ts;
private serviceHost: TypeScriptServiceHost;
private service: LanguageService;
private host: ts.LanguageServiceHost;
static 'extension-kind' = 'language-service';
constructor(config: {
ts: typeof ts; host: ts.LanguageServiceHost; service: ts.LanguageService;
registry?: ts.DocumentRegistry, args?: any
}) {
this.ts = config.ts;
this.host = config.host;
this.serviceHost = new TypeScriptServiceHost(this.ts, config.host, config.service);
this.service = createLanguageService(this.serviceHost);
this.serviceHost.setSite(this.service);
}
/**
* Augment the diagnostics reported by TypeScript with errors from the templates in string
* literals.
*/
getSemanticDiagnosticsFilter(fileName: string, previous: ts.Diagnostic[]): ts.Diagnostic[] {
let errors = this.service.getDiagnostics(fileName);
if (errors && errors.length) {
let file = this.serviceHost.getSourceFile(fileName);
for (const error of errors) {
previous.push({
file,
start: error.span.start,
length: error.span.end - error.span.start,
messageText: error.message,
category: this.ts.DiagnosticCategory.Error,
code: 0
});
}
}
return previous;
}
/**
* Get completions for angular templates if one is at the given position.
*/
getCompletionsAtPosition(fileName: string, position: number): ts.CompletionInfo {
let result = this.service.getCompletionsAt(fileName, position);
if (result) {
return {
isMemberCompletion: false,
isNewIdentifierLocation: false,
entries: result.map<ts.CompletionEntry>(
entry =>
({name: entry.name, kind: entry.kind, kindModifiers: '', sortText: entry.sort}))
};
}
}
}

View File

@ -0,0 +1,706 @@
/**
* @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, StaticSymbol} from '@angular/compiler';
import {NgAnalyzedModules} from '@angular/compiler/src/aot/compiler';
import {CompileMetadataResolver} from '@angular/compiler/src/metadata_resolver';
/**
* 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
* `LanguageSerivce` 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 */;
/**
* 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 error?: string;
}
/**
* 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,
/**
* 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 `LanguageSerivce` 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
* `LanguageSerivceHost`
* 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 `LanguageSerivce` 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,99 @@
/**
* @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} from '@angular/compiler';
import {ParseSourceSpan} from '@angular/compiler/src/parse_util';
import {CssSelector, SelectorMatcher} from '@angular/compiler/src/selector';
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 && diDep.token.identifier.name == '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,236 @@
/**
* @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 'reflect-metadata';
import * as ts from 'typescript';
import {createLanguageService} from '../src/language_service';
import {Completions, Diagnostic, Diagnostics} from '../src/types';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {toh} from './test_data';
import {MockTypescriptHost, includeDiagnostic, noDiagnostics} from './test_utils';
describe('completions', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
let ngHost = new TypeScriptServiceHost(ts, mockHost, service);
let ngService = createLanguageService(ngHost);
ngHost.setSite(ngService);
it('should be able to get entity completions',
() => { contains('/app/test.ng', 'entity-amp', '&amp;', '&gt;', '&lt;', '&iota;'); });
it('should be able to return html elements', () => {
let htmlTags = ['h1', 'h2', 'div', 'span'];
let locations = ['empty', 'start-tag-h1', 'h1-content', 'start-tag', 'start-tag-after-h'];
for (let location of locations) {
contains('/app/test.ng', location, ...htmlTags);
}
});
it('should be able to return element diretives',
() => { contains('/app/test.ng', 'empty', 'my-app'); });
it('should be able to return h1 attributes',
() => { contains('/app/test.ng', 'h1-after-space', 'id', 'dir', 'lang', 'onclick'); });
it('should be able to find common angular attributes',
() => { contains('/app/test.ng', 'div-attributes', '(click)', '[ngClass]'); });
it('should be able to get completions in some random garbage', () => {
const fileName = '/app/test.ng';
mockHost.override(fileName, ' > {{tle<\n {{retl ><bel/beled}}di>\n la</b </d &a ');
expect(() => ngService.getCompletionsAt(fileName, 31)).not.toThrow();
mockHost.override(fileName, undefined);
});
it('should be able to infer the type of a ngForOf', () => {
addCode(
`
interface Person {
name: string,
street: string
}
@Component({template: '<div *ngFor="let person of people">{{person.~{name}name}}</div'})
export class MyComponent {
people: Person[]
}`,
() => { contains('/app/app.component.ts', 'name', 'name', 'street'); });
});
it('should be able to infer the type of a ngForOf with an async pipe', () => {
addCode(
`
interface Person {
name: string,
street: string
}
@Component({template: '<div *ngFor="let person of people | async">{{person.~{name}name}}</div'})
export class MyComponent {
people: Promise<Person[]>;
}`,
() => { contains('/app/app.component.ts', 'name', 'name', 'street'); });
});
it('should be able to complete every character in the file', () => {
const fileName = '/app/test.ng';
expect(() => {
let chance = 0.05;
let requests = 0;
function tryCompletionsAt(position: number) {
try {
if (Math.random() < chance) {
ngService.getCompletionsAt(fileName, position);
requests++;
}
} catch (e) {
// Emit enough diagnostic information to reproduce the error.
console.log(
`Position: ${position}\nContent: "${mockHost.getFileContent(fileName)}"\nStack:\n${e.stack}`);
throw e;
}
}
try {
const originalContent = mockHost.getFileContent(fileName);
// For each character in the file, add it to the file and request a completion after it.
for (let index = 0, len = originalContent.length; index < len; index++) {
const content = originalContent.substr(0, index);
mockHost.override(fileName, content);
tryCompletionsAt(index);
}
// For the complete file, try to get a completion at every character.
mockHost.override(fileName, originalContent);
for (let index = 0, len = originalContent.length; index < len; index++) {
tryCompletionsAt(index);
}
// Delete random characters in the file until we get an empty file.
let content = originalContent;
while (content.length > 0) {
const deleteIndex = Math.floor(Math.random() * content.length);
content = content.slice(0, deleteIndex - 1) + content.slice(deleteIndex + 1);
mockHost.override(fileName, content);
const requestIndex = Math.floor(Math.random() * content.length);
tryCompletionsAt(requestIndex);
}
// Build up the string from zero asking for a completion after every char
buildUp(originalContent, (text, position) => {
mockHost.override(fileName, text);
tryCompletionsAt(position);
});
} finally {
mockHost.override(fileName, undefined);
}
}).not.toThrow();
});
describe('with regression tests', () => {
it('should not crash with an incomplete component', () => {
expect(() => {
const code = `
@Component({
template: '~{inside-template}'
})
export class MyComponent {
}`;
addCode(code, fileName => { contains(fileName, 'inside-template', 'h1'); });
}).not.toThrow();
});
it('should hot crash with an incomplete class', () => {
expect(() => {
addCode('\nexport class', fileName => { ngHost.updateAnalyzedModules(); });
}).not.toThrow();
});
});
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
const fileName = '/app/app.component.ts';
const originalContent = mockHost.getFileContent(fileName);
const newContent = originalContent + code;
mockHost.override(fileName, originalContent + code);
try {
cb(fileName, newContent);
} finally {
mockHost.override(fileName, undefined);
}
}
function contains(fileName: string, locationMarker: string, ...names: string[]) {
let location = mockHost.getMarkerLocations(fileName)[locationMarker];
if (location == null) {
throw new Error(`No marker ${locationMarker} found.`);
}
expectEntries(locationMarker, ngService.getCompletionsAt(fileName, location), ...names);
}
});
function expectEntries(locationMarker: string, completions: Completions, ...names: string[]) {
let entries: {[name: string]: boolean} = {};
if (!completions) {
throw new Error(
`Expected result from ${locationMarker} to include ${names.join(', ')} but no result provided`);
}
if (!completions.length) {
throw new Error(
`Expected result from ${locationMarker} to include ${names.join(', ')} an empty result provided`);
} else {
for (let entry of completions) {
entries[entry.name] = true;
}
let missing = names.filter(name => !entries[name]);
if (missing.length) {
throw new Error(
`Expected result from ${locationMarker} to include at least one of the following, ${missing.join(', ')}, in the list of entries ${completions.map(entry => entry.name).join(', ')}`);
}
}
}
function buildUp(originalText: string, cb: (text: string, position: number) => void) {
let count = originalText.length;
let inString: boolean[] = (new Array(count)).fill(false);
let unused: number[] = (new Array(count)).fill(1).map((v, i) => i);
function getText() {
return new Array(count)
.fill(1)
.map((v, i) => i)
.filter(i => inString[i])
.map(i => originalText[i])
.join('');
}
function randomUnusedIndex() { return Math.floor(Math.random() * unused.length); }
while (unused.length > 0) {
let unusedIndex = randomUnusedIndex();
let index = unused[unusedIndex];
if (index == null) throw new Error('Internal test buildup error');
if (inString[index]) throw new Error('Internal test buildup error');
inString[index] = true;
unused.splice(unusedIndex, 1);
let text = getText();
let position =
inString.filter((_, i) => i <= index).map(v => v ? 1 : 0).reduce((p, v) => p + v, 0);
cb(text, position);
}
}

View File

@ -0,0 +1,169 @@
/**
* @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 '../src/language_service';
import {Completions, Diagnostic, Diagnostics, Span} from '../src/types';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {toh} from './test_data';
import {MockTypescriptHost,} from './test_utils';
describe('definitions', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
let ngHost = new TypeScriptServiceHost(ts, mockHost, service);
let ngService = createLanguageService(ngHost);
ngHost.setSite(ngService);
it('should be able to find field in an interpolation', () => {
localReference(
` @Component({template: '{{«name»}}'}) export class MyComponent { «∆name∆: string;» }`);
});
it('should be able to find a field in a attribute reference', () => {
localReference(
` @Component({template: '<input [(ngModel)]="«name»">'}) export class MyComponent { «∆name∆: string;» }`);
});
it('should be able to find a method from a call', () => {
localReference(
` @Component({template: '<div (click)="«myClick»();"></div>'}) export class MyComponent { «∆myClick∆() { }»}`);
});
it('should be able to find a field reference in an *ngIf', () => {
localReference(
` @Component({template: '<div *ngIf="«include»"></div>'}) export class MyComponent { «∆include∆ = true;»}`);
});
it('should be able to find a reference to a component', () => {
reference(
'parsing-cases.ts',
` @Component({template: '<«test-comp»></test-comp>'}) export class MyComponent { }`);
});
it('should be able to find an event provider', () => {
reference(
'/app/parsing-cases.ts', 'test',
` @Component({template: '<test-comp («test»)="myHandler()"></div>'}) export class MyComponent { myHandler() {} }`);
});
it('should be able to find an input provider', () => {
reference(
'/app/parsing-cases.ts', 'tcName',
` @Component({template: '<test-comp [«tcName»]="name"></div>'}) export class MyComponent { name = 'my name'; }`);
});
it('should be able to find a pipe', () => {
reference(
'async_pipe.d.ts',
` @Component({template: '<div *ngIf="input | «async»"></div>'}) export class MyComponent { input: EventEmitter; }`);
});
function localReference(code: string) {
addCode(code, fileName => {
const refResult = mockHost.getReferenceMarkers(fileName);
for (const name in refResult.references) {
const references = refResult.references[name];
const definitions = refResult.definitions[name];
expect(definitions).toBeDefined(); // If this fails the test data is wrong.
for (const reference of references) {
const definition = ngService.getDefinitionAt(fileName, reference.start);
if (definition) {
definition.forEach(d => expect(d.fileName).toEqual(fileName));
const match = matchingSpan(definition.map(d => d.span), definitions);
if (!match) {
throw new Error(
`Expected one of ${stringifySpans(definition.map(d => d.span))} to match one of ${stringifySpans(definitions)}`);
}
} else {
throw new Error('Expected a definition');
}
}
}
});
}
function reference(referencedFile: string, code: string): void;
function reference(referencedFile: string, span: Span, code: string): void;
function reference(referencedFile: string, definition: string, code: string): void;
function reference(referencedFile: string, p1?: any, p2?: any): void {
const code: string = p2 ? p2 : p1;
const definition: string = p2 ? p1 : undefined;
let span: Span = p2 && p1.start != null ? p1 : undefined;
if (definition && !span) {
const referencedFileMarkers = mockHost.getReferenceMarkers(referencedFile);
expect(referencedFileMarkers).toBeDefined(); // If this fails the test data is wrong.
const spans = referencedFileMarkers.definitions[definition];
expect(spans).toBeDefined(); // If this fails the test data is wrong.
span = spans[0];
}
addCode(code, fileName => {
const refResult = mockHost.getReferenceMarkers(fileName);
let tests = 0;
for (const name in refResult.references) {
const references = refResult.references[name];
expect(reference).toBeDefined(); // If this fails the test data is wrong.
for (const reference of references) {
tests++;
const definition = ngService.getDefinitionAt(fileName, reference.start);
if (definition) {
definition.forEach(d => {
if (d.fileName.indexOf(referencedFile) < 0) {
throw new Error(
`Expected reference to file ${referencedFile}, received ${d.fileName}`);
}
if (span) {
expect(d.span).toEqual(span);
}
});
} else {
throw new Error('Expected a definition');
}
}
}
if (!tests) {
throw new Error('Expected at least one reference (test data error)');
}
});
}
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
const fileName = '/app/app.component.ts';
const originalContent = mockHost.getFileContent(fileName);
const newContent = originalContent + code;
mockHost.override(fileName, originalContent + code);
try {
cb(fileName, newContent);
} finally {
mockHost.override(fileName, undefined);
}
}
});
function matchingSpan(aSpans: Span[], bSpans: Span[]): Span {
for (const a of aSpans) {
for (const b of bSpans) {
if (a.start == b.start && a.end == b.end) {
return a;
}
}
}
}
function stringifySpan(span: Span) {
return span ? `(${span.start}-${span.end})` : '<undefined>';
}
function stringifySpans(spans: Span[]) {
return spans ? `[${spans.map(stringifySpan).join(', ')}]` : '<empty>';
}

View File

@ -0,0 +1,136 @@
/**
* @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 '../src/language_service';
import {Completions, Diagnostic, Diagnostics} from '../src/types';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {toh} from './test_data';
import {MockTypescriptHost, includeDiagnostic, noDiagnostics} from './test_utils';
describe('diagnostics', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
let ngHost = new TypeScriptServiceHost(ts, mockHost, service);
let ngService = createLanguageService(ngHost);
ngHost.setSite(ngService);
it('should be no diagnostics for test.ng',
() => { expect(ngService.getDiagnostics('/app/test.ng')).toEqual([]); });
describe('for semantic errors', () => {
const fileName = '/app/test.ng';
function diagnostics(template: string): Diagnostics {
try {
mockHost.override(fileName, template);
return ngService.getDiagnostics(fileName);
} finally {
mockHost.override(fileName, undefined);
}
}
function accept(template: string) { noDiagnostics(diagnostics(template)); }
function reject(template: string, message: string): void;
function reject(template: string, message: string, at: string): void;
function reject(template: string, message: string, location: string): void;
function reject(template: string, message: string, location: string, len: number): void;
function reject(template: string, message: string, at?: number | string, len?: number): void {
if (typeof at == 'string') {
len = at.length;
at = template.indexOf(at);
}
includeDiagnostic(diagnostics(template), message, at, len);
}
describe('with $event', () => {
it('should accept an event',
() => { accept('<div (click)="myClick($event)">Click me!</div>'); });
it('should reject it when not in an event binding', () => {
reject('<div [tabIndex]="$event"></div>', '\'$event\' is not defined', '$event');
});
});
});
describe('with regression tests', () => {
it('should not crash with a incomplete *ngFor', () => {
expect(() => {
const code =
'\n@Component({template: \'<div *ngFor></div> ~{after-div}\'}) export class MyComponent {}';
addCode(code, fileName => { ngService.getDiagnostics(fileName); });
}).not.toThrow();
});
it('should report a component not in a module', () => {
const code = '\n@Component({template: \'<div></div>\'}) export class MyComponent {}';
addCode(code, (fileName, content) => {
const diagnostics = ngService.getDiagnostics(fileName);
const offset = content.lastIndexOf('@Component') + 1;
const len = 'Component'.length;
includeDiagnostic(
diagnostics, 'Component \'MyComponent\' is not included in a module', offset, len);
});
});
it('should not report an error for a form\'s host directives', () => {
const code = '\n@Component({template: \'<form></form>\'}) export class MyComponent {}';
addCode(code, (fileName, content) => {
const diagnostics = ngService.getDiagnostics(fileName);
onlyModuleDiagnostics(diagnostics);
});
});
it('should not throw getting diagnostics for an index expression', () => {
const code =
` @Component({template: '<a *ngIf="(auth.isAdmin | async) || (event.leads && event.leads[(auth.uid | async)])"></a>'}) export class MyComponent {}`;
addCode(
code, fileName => { expect(() => ngService.getDiagnostics(fileName)).not.toThrow(); });
});
it('should not throw using a directive with no value', () => {
const code =
` @Component({template: '<form><input [(ngModel)]="name" required /></form>'}) export class MyComponent { name = 'some name'; }`;
addCode(
code, fileName => { expect(() => ngService.getDiagnostics(fileName)).not.toThrow(); });
});
it('should report an error for invalid metadata', () => {
const code =
` @Component({template: '', provider: [{provide: 'foo', useFactor: () => 'foo' }]}) export class MyComponent { name = 'some name'; }`;
addCode(code, (fileName, content) => {
const diagnostics = ngService.getDiagnostics(fileName);
includeDiagnostic(
diagnostics, 'Function calls are not supported.', '() => \'foo\'', content);
});
});
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
const fileName = '/app/app.component.ts';
const originalContent = mockHost.getFileContent(fileName);
const newContent = originalContent + code;
mockHost.override(fileName, originalContent + code);
try {
cb(fileName, newContent);
} finally {
mockHost.override(fileName, undefined);
}
}
function onlyModuleDiagnostics(diagnostics: Diagnostics) {
// Expect only the 'MyComponent' diagnostic
expect(diagnostics.length).toBe(1);
expect(diagnostics[0].message.indexOf('MyComponent') >= 0).toBeTruthy();
}
});
});

View File

@ -0,0 +1,105 @@
/**
* @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 'reflect-metadata';
import * as ts from 'typescript';
import {createLanguageService} from '../src/language_service';
import {Hover, HoverTextSection} from '../src/types';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {toh} from './test_data';
import {MockTypescriptHost, includeDiagnostic, noDiagnostics} from './test_utils';
describe('hover', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
let ngHost = new TypeScriptServiceHost(ts, mockHost, service);
let ngService = createLanguageService(ngHost);
ngHost.setSite(ngService);
it('should be able to find field in an interpolation', () => {
hover(
` @Component({template: '{{«name»}}'}) export class MyComponent { name: string; }`,
'property name of MyComponent');
});
it('should be able to find a field in a attribute reference', () => {
hover(
` @Component({template: '<input [(ngModel)]="«name»">'}) export class MyComponent { name: string; }`,
'property name of MyComponent');
});
it('should be able to find a method from a call', () => {
hover(
` @Component({template: '<div (click)="«∆myClick∆()»;"></div>'}) export class MyComponent { myClick() { }}`,
'method myClick of MyComponent');
});
it('should be able to find a field reference in an *ngIf', () => {
hover(
` @Component({template: '<div *ngIf="«include»"></div>'}) export class MyComponent { include = true;}`,
'property include of MyComponent');
});
it('should be able to find a reference to a component', () => {
hover(
` @Component({template: '«<∆test∆-comp></test-comp>»'}) export class MyComponent { }`,
'component TestComponent');
});
it('should be able to find an event provider', () => {
hover(
` @Component({template: '<test-comp «(∆test∆)="myHandler()"»></div>'}) export class MyComponent { myHandler() {} }`,
'event testEvent of TestComponent');
});
it('should be able to find an input provider', () => {
hover(
` @Component({template: '<test-comp «[∆tcName∆]="name"»></div>'}) export class MyComponent { name = 'my name'; }`,
'property name of TestComponent');
});
function hover(code: string, hoverText: string) {
addCode(code, fileName => {
let tests = 0;
const markers = mockHost.getReferenceMarkers(fileName);
const keys = Object.keys(markers.references).concat(Object.keys(markers.definitions));
for (const referenceName of keys) {
const references = (markers.references[referenceName] ||
[]).concat(markers.definitions[referenceName] || []);
for (const reference of references) {
tests++;
const hover = ngService.getHoverAt(fileName, reference.start);
if (!hover) throw new Error(`Expected a hover at location ${reference.start}`);
expect(hover.span).toEqual(reference);
expect(toText(hover)).toEqual(hoverText);
}
}
expect(tests).toBeGreaterThan(0); // If this fails the test is wrong.
});
}
function addCode(code: string, cb: (fileName: string, content?: string) => void) {
const fileName = '/app/app.component.ts';
const originalContent = mockHost.getFileContent(fileName);
const newContent = originalContent + code;
mockHost.override(fileName, originalContent + code);
try {
cb(fileName, newContent);
} finally {
mockHost.override(fileName, undefined);
}
}
function toText(hover: Hover): string { return hover.text.map(h => h.text).join(''); }
});

View File

@ -0,0 +1,52 @@
/**
* @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 {DomElementSchemaRegistry} from '@angular/compiler';
import {SchemaInformation} from '../src/html_info';
describe('html_info', () => {
const domRegistry = new DomElementSchemaRegistry();
it('should have the same elements as the dom registry', () => {
// If this test fails, replace the SCHEMA constant in html_info with the one
// from dom_element_schema_registry and also verify the code to interpret
// the schema is the same.
const domElements = domRegistry.allKnownElementNames();
const infoElements = SchemaInformation.instance.allKnownElements();
const uniqueToDom = uniqueElements(infoElements, domElements);
const uniqueToInfo = uniqueElements(domElements, infoElements);
expect(uniqueToDom).toEqual([]);
expect(uniqueToInfo).toEqual([]);
});
it('should have at least a sub-set of properties', () => {
const elements = SchemaInformation.instance.allKnownElements();
for (const element of elements) {
for (const prop of SchemaInformation.instance.propertiesOf(element)) {
expect(domRegistry.hasProperty(element, prop, []));
}
}
});
});
function uniqueElements<T>(a: T[], b: T[]): T[] {
const s = new Set<T>();
for (const aItem of a) {
s.add(aItem);
}
const result: T[] = [];
const reported = new Set<T>();
for (const bItem of b) {
if (!s.has(bItem) && !reported.has(bItem)) {
reported.add(bItem);
result.push(bItem);
}
}
return result;
}

View File

@ -0,0 +1,32 @@
/**
* @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 '../src/language_service';
import {Completions, Diagnostic, Diagnostics} from '../src/types';
import {TypeScriptServiceHost} from '../src/typescript_host';
import {toh} from './test_data';
import {MockTypescriptHost} from './test_utils';
describe('references', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
let ngHost = new TypeScriptServiceHost(ts, mockHost, service);
let ngService = createLanguageService(ngHost);
ngHost.setSite(ngService);
it('should be able to get template references',
() => { expect(() => ngService.getTemplateReferences()).not.toThrow(); });
it('should be able to determine that test.ng is a template reference',
() => { expect(ngService.getTemplateReferences()).toContain('/app/test.ng'); });
});

View File

@ -0,0 +1,231 @@
/**
* @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 {MockData} from './test_utils';
export const toh = {
app: {
'app.component.ts': `import { Component } from '@angular/core';
export class Hero {
id: number;
name: string;
}
@Component({
selector: 'my-app',
template: \`~{empty}
<~{start-tag}h~{start-tag-after-h}1~{start-tag-h1} ~{h1-after-space}>~{h1-content} {{~{sub-start}title~{sub-end}}}</h1>
~{after-h1}<h2>{{~{h2-hero}hero.~{h2-name}name}} details!</h2>
<div><label>id: </label>{{~{label-hero}hero.~{label-id}id}}</div>
<div ~{div-attributes}>
<label>name: </label>
</div>
&~{entity-amp}amp;
\`
})
export class AppComponent {
title = 'Tour of Heroes';
hero: Hero = {
id: 1,
name: 'Windstorm'
};
private internal: string;
}`,
'main.ts': `
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { CaseIncompleteOpen, CaseMissingClosing, CaseUnknown, Pipes, TemplateReference, NoValueAttribute,
AttributeBinding, StringModel,PropertyBinding, EventBinding, TwoWayBinding, EmptyInterpolation,
ForOfEmpty, ForLetIEqual, ForOfLetEmpty, ForUsingComponent, References, TestComponent} from './parsing-cases';
import { WrongFieldReference, WrongSubFieldReference, PrivateReference, ExpectNumericType, LowercasePipe } from './expression-cases';
import { UnknownPeople, UnknownEven, UnknownTrackBy } from './ng-for-cases';
import { ShowIf } from './ng-if-cases';
@NgModule({
imports: [CommonModule, FormsModule],
declarations: [AppComponent, CaseIncompleteOpen, CaseMissingClosing, CaseUnknown, Pipes, TemplateReference, NoValueAttribute,
AttributeBinding, StringModel, PropertyBinding, EventBinding, TwoWayBinding, EmptyInterpolation, ForOfEmpty, ForOfLetEmpty,
ForLetIEqual, ForUsingComponent, References, TestComponent, WrongFieldReference, WrongSubFieldReference, PrivateReference,
ExpectNumericType, UnknownPeople, UnknownEven, UnknownTrackBy, ShowIf, LowercasePipe]
})
export class AppModule {}
declare function bootstrap(v: any): void;
bootstrap(AppComponent);
`,
'parsing-cases.ts': `
import {Component, Directive, Input, Output, EventEmitter} from '@angular/core';
import {Hero} from './app.component';
@Component({template: '<h1>Some <~{incomplete-open-lt}a~{incomplete-open-a} ~{incomplete-open-attr} text</h1>'})
export class CaseIncompleteOpen {}
@Component({template: '<h1>Some <a> ~{missing-closing} text</h1>'})
export class CaseMissingClosing {}
@Component({template: '<h1>Some <unknown ~{unknown-element}> text</h1>'})
export class CaseUnknown {}
@Component({template: '<h1>{{data | ~{before-pipe}lowe~{in-pipe}rcase~{after-pipe} }}'})
export class Pipes {
data = 'Some string';
}
@Component({template: '<h1 h~{no-value-attribute}></h1>'})
export class NoValueAttribute {}
@Component({template: '<h1 model="~{attribute-binding-model}test"></h1>'})
export class AttributeBinding {
test: string;
}
@Component({template: '<h1 [model]="~{property-binding-model}test"></h1>'})
export class PropertyBinding {
test: string;
}
@Component({template: '<h1 (model)="~{event-binding-model}modelChanged()"></h1>'})
export class EventBinding {
test: string;
modelChanged() {}
}
@Component({template: '<h1 [(model)]="~{two-way-binding-model}test"></h1>'})
export class TwoWayBinding {
test: string;
}
@Directive({selector: '[string-model]'})
export class StringModel {
@Input() model: string;
@Output() modelChanged: EventEmitter<string>;
}
interface Person {
name: string;
age: number
}
@Component({template: '<div *ngFor="~{for-empty}"></div>'})
export class ForOfEmpty {}
@Component({template: '<div *ngFor="let ~{for-let-empty}"></div>'})
export class ForOfLetEmpty {}
@Component({template: '<div *ngFor="let i = ~{for-let-i-equal}"></div>'})
export class ForLetIEqual {}
@Component({template: '<div *ngFor="~{for-let}let ~{for-person}person ~{for-of}of ~{for-people}people"> <span>Name: {{~{for-interp-person}person.~{for-interp-name}name}}</span><span>Age: {{person.~{for-interp-age}age}}</span></div>'})
export class ForUsingComponent {
people: Person[];
}
@Component({template: '<div #div> <test-comp #test1> {{~{test-comp-content}}} {{test1.~{test-comp-after-test}name}} {{div.~{test-comp-after-div}.innerText}} </test-comp> </div> <test-comp #test2></test-comp>'})
export class References {}
@Component({selector: 'test-comp', template: '<div>Testing: {{name}}</div>'})
export class TestComponent {
«@Input('∆tcName∆') name = 'test';»
«@Output('∆test∆') testEvent = new EventEmitter();»
}
@Component({templateUrl: 'test.ng'})
export class TemplateReference {
title = 'Some title';
hero: Hero = {
id: 1,
name: 'Windstorm'
};
myClick(event: any) {
}
}
@Component({template: '{{~{empty-interpolation}}}'})
export class EmptyInterpolation {
title = 'Some title';
subTitle = 'Some sub title';
}
`,
'expression-cases.ts': `
import {Component} from '@angular/core';
export interface Person {
name: string;
age: number;
}
@Component({template: '{{~{foo}foo~{foo-end}}}'})
export class WrongFieldReference {
bar = 'bar';
}
@Component({template: '{{~{nam}person.nam~{nam-end}}}'})
export class WrongSubFieldReference {
person: Person = { name: 'Bob', age: 23 };
}
@Component({template: '{{~{myField}myField~{myField-end}}}'})
export class PrivateReference {
private myField = 'My Field';
}
@Component({template: '{{~{mod}"a" ~{mod-end}% 2}}'})
export class ExpectNumericType {}
@Component({template: '{{ (name | lowercase).~{string-pipe}substring }}'})
export class LowercasePipe {
name: string;
}
`,
'ng-for-cases.ts': `
import {Component} from '@angular/core';
export interface Person {
name: string;
age: number;
}
@Component({template: '<div *ngFor="let person of ~{people_1}people_1~{people_1-end}"> <span>{{person.name}}</span> </div>'})
export class UnknownPeople {}
@Component({template: '<div ~{even_1}*ngFor="let person of people; let e = even_1"~{even_1-end}><span>{{person.name}}</span> </div>'})
export class UnknownEven {
people: Person[];
}
@Component({template: '<div *ngFor="let person of people; trackBy ~{trackBy_1}trackBy_1~{trackBy_1-end}"><span>{{person.name}}</span> </div>'})
export class UnknownTrackBy {
people: Person[];
}
`,
'ng-if-cases.ts': `
import {Component} from '@angular/core';
@Component({template: '<div ~{implicit}*ngIf="show; let l"~{implicit-end}>Showing now!</div>'})
export class ShowIf {
show = false;
}
`,
'test.ng': `~{empty}
<~{start-tag}h~{start-tag-after-h}1~{start-tag-h1} ~{h1-after-space}>~{h1-content} {{~{sub-start}title~{sub-end}}}</h1>
~{after-h1}<h2>{{~{h2-hero}hero.~{h2-name}name}} details!</h2>
<div><label>id: </label>{{~{label-hero}hero.~{label-id}id}}</div>
<div ~{div-attributes}>
<label>name: </label>
</div>
&~{entity-amp}amp;
`
}
};

View File

@ -0,0 +1,320 @@
/**
* @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
*/
/// <reference path="../../../../node_modules/@types/node/index.d.ts" />
/// <reference path="../../../../node_modules/@types/jasmine/index.d.ts" />
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {Diagnostic, Diagnostics, Span} from '../src/types';
export type MockData = string | MockDirectory;
export type MockDirectory = {
[name: string]: MockData | undefined;
}
const angularts = /@angular\/(\w|\/|-)+\.tsx?$/;
const rxjsts = /rxjs\/(\w|\/)+\.tsx?$/;
const rxjsmetadata = /rxjs\/(\w|\/)+\.metadata\.json?$/;
const tsxfile = /\.tsx$/;
/* The missing cache does two things. First it improves performance of the
tests as it reduces the number of OS calls made during testing. Also it
improves debugging experience as fewer exceptions are raised allow you
to use stopping on all exceptions. */
const missingCache = new Map<string, boolean>();
const cacheUsed = new Set<string>();
const reportedMissing = new Set<string>();
/**
* The cache is valid if all the returned entries are empty.
*/
export function validateCache(): {exists: string[], unused: string[], reported: string[]} {
const exists: string[] = [];
const unused: string[] = [];
for (const fileName of iterableToArray(missingCache.keys())) {
if (fs.existsSync(fileName)) {
exists.push(fileName);
}
if (!cacheUsed.has(fileName)) {
unused.push(fileName);
}
}
return {exists, unused, reported: iterableToArray(reportedMissing.keys())};
}
missingCache.set('/node_modules/@angular/core.d.ts', true);
missingCache.set('/node_modules/@angular/common.d.ts', true);
missingCache.set('/node_modules/@angular/forms.d.ts', true);
missingCache.set('/node_modules/@angular/core/src/di/provider.metadata.json', true);
missingCache.set(
'/node_modules/@angular/core/src/change_detection/pipe_transform.metadata.json', true);
missingCache.set('/node_modules/@angular/core/src/reflection/types.metadata.json', true);
missingCache.set(
'/node_modules/@angular/core/src/reflection/platform_reflection_capabilities.metadata.json',
true);
missingCache.set('/node_modules/@angular/forms/src/directives/form_interface.metadata.json', true);
export class MockTypescriptHost implements ts.LanguageServiceHost {
private angularPath: string;
private nodeModulesPath: string;
private scriptVersion = new Map<string, number>();
private overrides = new Map<string, string>();
private projectVersion = 0;
constructor(private scriptNames: string[], private data: MockData) {
let angularIndex = module.filename.indexOf('@angular');
if (angularIndex >= 0)
this.angularPath =
module.filename.substr(0, angularIndex).replace('/all/', '/packages-dist/');
let distIndex = module.filename.indexOf('/dist/all');
if (distIndex >= 0)
this.nodeModulesPath = path.join(module.filename.substr(0, distIndex), 'node_modules');
}
override(fileName: string, content: string) {
this.scriptVersion.set(fileName, (this.scriptVersion.get(fileName) || 0) + 1);
if (fileName.endsWith('.ts')) {
this.projectVersion++;
}
if (content) {
this.overrides.set(fileName, content);
} else {
this.overrides.delete(fileName);
}
}
getCompilationSettings(): ts.CompilerOptions {
return {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.CommonJS,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
emitDecoratorMetadata: true,
experimentalDecorators: true,
removeComments: false,
noImplicitAny: false,
lib: ['lib.es2015.d.ts', 'lib.dom.d.ts'],
};
}
getProjectVersion(): string { return this.projectVersion.toString(); }
getScriptFileNames(): string[] { return this.scriptNames; }
getScriptVersion(fileName: string): string {
return (this.scriptVersion.get(fileName) || 0).toString();
}
getScriptSnapshot(fileName: string): ts.IScriptSnapshot {
const content = this.getFileContent(fileName);
if (content) return ts.ScriptSnapshot.fromString(content);
return undefined;
}
getCurrentDirectory(): string { return '/'; }
getDefaultLibFileName(options: ts.CompilerOptions): string { return 'lib.d.ts'; }
directoryExists(directoryName: string): boolean {
let effectiveName = this.getEffectiveName(directoryName);
if (effectiveName === directoryName)
return directoryExists(directoryName, this.data);
else
return fs.existsSync(effectiveName);
}
getMarkerLocations(fileName: string): {[name: string]: number}|undefined {
let content = this.getRawFileContent(fileName);
if (content) {
return getLocationMarkers(content);
}
}
getReferenceMarkers(fileName: string): ReferenceResult {
let content = this.getRawFileContent(fileName);
if (content) {
return getReferenceMarkers(content);
}
}
getFileContent(fileName: string): string {
const content = this.getRawFileContent(fileName);
if (content) return removeReferenceMarkers(removeLocationMarkers(content));
}
private getRawFileContent(fileName: string): string {
if (this.overrides.has(fileName)) {
return this.overrides.get(fileName);
}
let basename = path.basename(fileName);
if (/^lib.*\.d\.ts$/.test(basename)) {
let libPath = ts.getDefaultLibFilePath(this.getCompilationSettings());
return fs.readFileSync(path.join(path.dirname(libPath), basename), 'utf8');
} else {
if (missingCache.has(fileName)) {
cacheUsed.add(fileName);
return undefined;
}
let effectiveName = this.getEffectiveName(fileName);
if (effectiveName === fileName)
return open(fileName, this.data);
else if (
!fileName.match(angularts) && !fileName.match(rxjsts) && !fileName.match(rxjsmetadata) &&
!fileName.match(tsxfile)) {
if (fs.existsSync(effectiveName)) {
return fs.readFileSync(effectiveName, 'utf8');
} else {
missingCache.set(fileName, true);
reportedMissing.add(fileName);
cacheUsed.add(fileName);
}
}
}
}
private getEffectiveName(name: string): string {
const node_modules = 'node_modules';
const at_angular = '/@angular';
if (name.startsWith('/' + node_modules)) {
if (this.nodeModulesPath && !name.startsWith('/' + node_modules + at_angular)) {
let result = path.join(this.nodeModulesPath, name.substr(node_modules.length + 1));
if (!name.match(rxjsts))
if (fs.existsSync(result)) {
return result;
}
}
if (this.angularPath && name.startsWith('/' + node_modules + at_angular)) {
return path.join(
this.angularPath, name.substr(node_modules.length + at_angular.length + 1));
}
}
return name;
}
}
function iterableToArray<T>(iterator: IterableIterator<T>) {
const result: T[] = [];
while (true) {
const next = iterator.next();
if (next.done) break;
result.push(next.value);
}
return result;
}
function find(fileName: string, data: MockData): MockData|undefined {
let names = fileName.split('/');
if (names.length && !names[0].length) names.shift();
let current = data;
for (let name of names) {
if (typeof current === 'string')
return undefined;
else
current = (<MockDirectory>current)[name];
if (!current) return undefined;
}
return current;
}
function open(fileName: string, data: MockData): string|undefined {
let result = find(fileName, data);
if (typeof result === 'string') {
return result;
}
return undefined;
}
function directoryExists(dirname: string, data: MockData): boolean {
let result = find(dirname, data);
return result && typeof result !== 'string';
}
const locationMarker = /\~\{(\w+(-\w+)*)\}/g;
function removeLocationMarkers(value: string): string {
return value.replace(locationMarker, '');
}
function getLocationMarkers(value: string): {[name: string]: number} {
value = removeReferenceMarkers(value);
let result: {[name: string]: number} = {};
let adjustment = 0;
value.replace(locationMarker, (match: string, name: string, _: any, index: number): string => {
result[name] = index - adjustment;
adjustment += match.length;
return '';
});
return result;
}
const referenceMarker = /«(((\w|\-)+)|([^∆]*∆(\w+)∆.[^»]*))»/g;
const definitionMarkerGroup = 1;
const nameMarkerGroup = 2;
export type ReferenceMarkers = {
[name: string]: Span[]
};
export interface ReferenceResult {
text: string;
definitions: ReferenceMarkers;
references: ReferenceMarkers;
}
function getReferenceMarkers(value: string): ReferenceResult {
const references: ReferenceMarkers = {};
const definitions: ReferenceMarkers = {};
value = removeLocationMarkers(value);
let adjustment = 0;
const text = value.replace(
referenceMarker, (match: string, text: string, reference: string, _: string,
definition: string, definitionName: string, index: number): string => {
const result = reference ? text : text.replace(/∆/g, '');
const span: Span = {start: index - adjustment, end: index - adjustment + result.length};
const markers = reference ? references : definitions;
const name = reference || definitionName;
(markers[name] = (markers[name] || [])).push(span);
adjustment += match.length - result.length;
return result;
});
return {text, definitions, references};
}
function removeReferenceMarkers(value: string): string {
return value.replace(referenceMarker, (match, text) => text.replace(/∆/g, ''));
}
export function noDiagnostics(diagnostics: Diagnostics) {
if (diagnostics && diagnostics.length) {
throw new Error(`Unexpected diagnostics: \n ${diagnostics.map(d => d.message).join('\n ')}`);
}
}
export function includeDiagnostic(
diagnostics: Diagnostics, message: string, text?: string, len?: string): void;
export function includeDiagnostic(
diagnostics: Diagnostics, message: string, at?: number, len?: number): void;
export function includeDiagnostic(diagnostics: Diagnostics, message: string, p1?: any, p2?: any) {
expect(diagnostics).toBeDefined();
if (diagnostics) {
const diagnostic = diagnostics.find(d => d.message.indexOf(message) >= 0) as Diagnostic;
expect(diagnostic).toBeDefined();
if (diagnostic && p1 != null) {
const at = typeof p1 === 'number' ? p1 : p2.indexOf(p1);
const len = typeof p2 === 'number' ? p2 : p1.length;
expect(diagnostic.span.start).toEqual(at);
if (len != null) {
expect(diagnostic.span.end - diagnostic.span.start).toEqual(len);
}
}
}
}

View File

@ -0,0 +1,264 @@
/**
* @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 'reflect-metadata';
import * as ts from 'typescript';
import {LanguageServicePlugin} from '../src/ts_plugin';
import {toh} from './test_data';
import {MockTypescriptHost} from './test_utils';
describe('plugin', () => {
let documentRegistry = ts.createDocumentRegistry();
let mockHost = new MockTypescriptHost(['/app/main.ts', '/app/parsing-cases.ts'], toh);
let service = ts.createLanguageService(mockHost, documentRegistry);
let program = service.getProgram();
it('should not report errors on tour of heroes', () => {
expectNoDiagnostics(service.getCompilerOptionsDiagnostics());
for (let source of program.getSourceFiles()) {
expectNoDiagnostics(service.getSyntacticDiagnostics(source.fileName));
expectNoDiagnostics(service.getSemanticDiagnostics(source.fileName));
}
});
let plugin =
new LanguageServicePlugin({ts: ts, host: mockHost, service, registry: documentRegistry});
it('should not report template errors on tour of heroes', () => {
for (let source of program.getSourceFiles()) {
// Ignore all 'cases.ts' files as they intentionally contain errors.
if (!source.fileName.endsWith('cases.ts')) {
expectNoDiagnostics(plugin.getSemanticDiagnosticsFilter(source.fileName, []));
}
}
});
it('should be able to get entity completions',
() => { contains('app/app.component.ts', 'entity-amp', '&amp;', '&gt;', '&lt;', '&iota;'); });
it('should be able to return html elements', () => {
let htmlTags = ['h1', 'h2', 'div', 'span'];
let locations = ['empty', 'start-tag-h1', 'h1-content', 'start-tag', 'start-tag-after-h'];
for (let location of locations) {
contains('app/app.component.ts', location, ...htmlTags);
}
});
it('should be able to return element diretives',
() => { contains('app/app.component.ts', 'empty', 'my-app'); });
it('should be able to return h1 attributes',
() => { contains('app/app.component.ts', 'h1-after-space', 'id', 'dir', 'lang', 'onclick'); });
it('should be able to find common angular attributes', () => {
contains('app/app.component.ts', 'div-attributes', '(click)', '[ngClass]', '*ngIf', '*ngFor');
});
it('should be able to returned attribute names with an incompete attribute',
() => { contains('app/parsing-cases.ts', 'no-value-attribute', 'id', 'dir', 'lang'); });
it('should be able to return attributes of an incomplete element', () => {
contains('app/parsing-cases.ts', 'incomplete-open-lt', 'a');
contains('app/parsing-cases.ts', 'incomplete-open-a', 'a');
contains('app/parsing-cases.ts', 'incomplete-open-attr', 'id', 'dir', 'lang');
});
it('should be able to return completions with a missing closing tag',
() => { contains('app/parsing-cases.ts', 'missing-closing', 'h1', 'h2'); });
it('should be able to return common attributes of in an unknown tag',
() => { contains('app/parsing-cases.ts', 'unknown-element', 'id', 'dir', 'lang'); });
it('should be able to get the completions at the beginning of an interpolation',
() => { contains('app/app.component.ts', 'h2-hero', 'hero', 'title'); });
it('should not include private members of the of a class',
() => { contains('app/app.component.ts', 'h2-hero', '-internal'); });
it('should be able to get the completions at the end of an interpolation',
() => { contains('app/app.component.ts', 'sub-end', 'hero', 'title'); });
it('should be able to get the completions in a property read',
() => { contains('app/app.component.ts', 'h2-name', 'name', 'id'); });
it('should be able to get a list of pipe values', () => {
contains('app/parsing-cases.ts', 'before-pipe', 'lowercase', 'uppercase');
contains('app/parsing-cases.ts', 'in-pipe', 'lowercase', 'uppercase');
contains('app/parsing-cases.ts', 'after-pipe', 'lowercase', 'uppercase');
});
it('should be able get completions in an empty interpolation',
() => { contains('app/parsing-cases.ts', 'empty-interpolation', 'title', 'subTitle'); });
describe('with attributes', () => {
it('should be able to complete property value',
() => { contains('app/parsing-cases.ts', 'property-binding-model', 'test'); });
it('should be able to complete an event',
() => { contains('app/parsing-cases.ts', 'event-binding-model', 'modelChanged'); });
it('should be able to complete a two-way binding',
() => { contains('app/parsing-cases.ts', 'two-way-binding-model', 'test'); });
});
describe('with a *ngFor', () => {
it('should include a let for empty attribute',
() => { contains('app/parsing-cases.ts', 'for-empty', 'let'); });
it('should not suggest any entries if in the name part of a let',
() => { expectEmpty('app/parsing-cases.ts', 'for-let-empty'); });
it('should suggest NgForRow members for let initialization expression', () => {
contains(
'app/parsing-cases.ts', 'for-let-i-equal', 'index', 'count', 'first', 'last', 'even',
'odd');
});
it('should include a let', () => { contains('app/parsing-cases.ts', 'for-let', 'let'); });
it('should include an "of"', () => { contains('app/parsing-cases.ts', 'for-of', 'of'); });
it('should include field reference',
() => { contains('app/parsing-cases.ts', 'for-people', 'people'); });
it('should include person in the let scope',
() => { contains('app/parsing-cases.ts', 'for-interp-person', 'person'); });
// TODO: Enable when we can infer the element type of the ngFor
// it('should include determine person\'s type as Person', () => {
// contains('app/parsing-cases.ts', 'for-interp-name', 'name', 'age');
// contains('app/parsing-cases.ts', 'for-interp-age', 'name', 'age');
// });
});
describe('for pipes', () => {
it('should be able to resolve lowercase',
() => { contains('app/expression-cases.ts', 'string-pipe', 'substring'); });
});
describe('with references', () => {
it('should list references',
() => { contains('app/parsing-cases.ts', 'test-comp-content', 'test1', 'test2', 'div'); });
it('should reference the component',
() => { contains('app/parsing-cases.ts', 'test-comp-after-test', 'name'); });
// TODO: Enable when we have a flag that indicates the project targets the DOM
// it('should refernce the element if no component', () => {
// contains('app/parsing-cases.ts', 'test-comp-after-div', 'innerText');
// });
});
describe('for semantic errors', () => {
it('should report access to an unknown field', () => {
expectSemanticError(
'app/expression-cases.ts', 'foo',
'Identifier \'foo\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member');
});
it('should report access to an unknown sub-field', () => {
expectSemanticError(
'app/expression-cases.ts', 'nam',
'Identifier \'nam\' is not defined. \'Person\' does not contain such a member');
});
it('should report access to a private member', () => {
expectSemanticError(
'app/expression-cases.ts', 'myField',
'Identifier \'myField\' refers to a private member of the component');
});
it('should report numeric operator erros',
() => { expectSemanticError('app/expression-cases.ts', 'mod', 'Expected a numeric type'); });
describe('in ngFor', () => {
function expectError(locationMarker: string, message: string) {
expectSemanticError('app/ng-for-cases.ts', locationMarker, message);
}
it('should report an unknown field', () => {
expectError(
'people_1',
'Identifier \'people_1\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member');
});
it('should report an unknown context reference', () => {
expectError('even_1', 'The template context does not defined a member called \'even_1\'');
});
it('should report an unknown value in a key expression', () => {
expectError(
'trackBy_1',
'Identifier \'trackBy_1\' is not defined. The component declaration, template variable declarations, and element references do not contain such a member');
});
});
describe('in ngIf', () => {
function expectError(locationMarker: string, message: string) {
expectSemanticError('app/ng-if-cases.ts', locationMarker, message);
}
it('should report an implicit context reference', () => {
expectError('implicit', 'The template context does not have an implicit value');
});
});
});
function getMarkerLocation(fileName: string, locationMarker: string): number {
const location = mockHost.getMarkerLocations(fileName)[locationMarker];
if (location == null) {
throw new Error(`No marker ${locationMarker} found.`);
}
return location;
}
function contains(fileName: string, locationMarker: string, ...names: string[]) {
const location = getMarkerLocation(fileName, locationMarker);
expectEntries(locationMarker, plugin.getCompletionsAtPosition(fileName, location), ...names);
}
function expectEmpty(fileName: string, locationMarker: string) {
const location = getMarkerLocation(fileName, locationMarker);
expect(plugin.getCompletionsAtPosition(fileName, location).entries).toEqual([]);
}
function expectSemanticError(fileName: string, locationMarker: string, message: string) {
const start = getMarkerLocation(fileName, locationMarker);
const end = getMarkerLocation(fileName, locationMarker + '-end');
const errors = plugin.getSemanticDiagnosticsFilter(fileName, []);
for (const error of errors) {
if (error.messageText.toString().indexOf(message) >= 0) {
expect(error.start).toEqual(start);
expect(error.length).toEqual(end - start);
return;
}
}
throw new Error(
`Expected error messages to contain ${message}, in messages:\n ${errors.map(e => e.messageText.toString()).join(',\n ')}`);
}
});
function expectEntries(locationMarker: string, info: ts.CompletionInfo, ...names: string[]) {
let entries: {[name: string]: boolean} = {};
if (!info) {
throw new Error(
`Expected result from ${locationMarker} to include ${names.join(', ')} but no result provided`);
} else {
for (let entry of info.entries) {
entries[entry.name] = true;
}
let shouldContains = names.filter(name => !name.startsWith('-'));
let shouldNotContain = names.filter(name => name.startsWith('-'));
let missing = shouldContains.filter(name => !entries[name]);
let present = shouldNotContain.map(name => name.substr(1)).filter(name => entries[name]);
if (missing.length) {
throw new Error(
`Expected result from ${locationMarker} to include at least one of the following, ${missing.join(', ')}, in the list of entries ${info.entries.map(entry => entry.name).join(', ')}`);
}
if (present.length) {
throw new Error(
`Unexpected member${present.length > 1 ? 's': ''} included in result: ${present.join(', ')}`);
}
}
}
function expectNoDiagnostics(diagnostics: ts.Diagnostic[]) {
for (const diagnostic of diagnostics) {
let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
if (diagnostic.start) {
let {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
} else {
console.log(`${message}`);
}
}
expect(diagnostics.length).toBe(0);
}

View File

@ -0,0 +1,36 @@
{
"compilerOptions": {
"baseUrl": ".",
"declaration": true,
"stripInternal": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"module": "es2015",
"moduleResolution": "node",
"outDir": "../../../dist/packages-dist/language-service",
"paths": {
"@angular/core": ["../../../dist/packages-dist/core"],
"@angular/core/testing": ["../../../dist/packages-dist/core/testing"],
"@angular/core/testing/*": ["../../../dist/packages-dist/core/testing/*"],
"@angular/common": ["../../../dist/packages-dist/common"],
"@angular/compiler": ["../../../dist/packages-dist/compiler"],
"@angular/compiler/*": ["../../../dist/packages-dist/compiler/*"],
"@angular/platform-server": ["../../../dist/packages-dist/platform-server"],
"@angular/platform-browser": ["../../../dist/packages-dist/platform-browser"],
"@angular/tsc-wrapped": ["../../../dist/tools/@angular/tsc-wrapped"],
"@angular/tsc-wrapped/*": ["../../../dist/tools/@angular/tsc-wrapped/*"]
},
"rootDir": ".",
"sourceMap": true,
"inlineSources": true,
"target": "es5",
"skipLibCheck": true,
"lib": ["es2015", "dom"]
},
"files": [
"index.ts",
"../../../node_modules/zone.js/dist/zone.js.d.ts",
"../../../node_modules/@types/node/index.d.ts",
"../../../node_modules/@types/jasmine/index.d.ts"
]
}

View File

@ -13,7 +13,8 @@
"selenium-webdriver": ["../node_modules/@types/selenium-webdriver/index.d.ts"],
"rxjs/*": ["../node_modules/rxjs/*"],
"@angular/*": ["./@angular/*"],
"@angular/tsc-wrapped": ["../dist/tools/@angular/tsc-wrapped"]
"@angular/tsc-wrapped": ["../dist/tools/@angular/tsc-wrapped"],
"@angular/tsc-wrapped/*": ["../dist/tools/@angular/tsc-wrapped/*"]
},
"rootDir": ".",
"inlineSourceMap": true,

View File

@ -1853,6 +1853,9 @@
"esprima": {
"version": "2.7.1"
},
"estree-walker": {
"version": "0.2.1"
},
"etag": {
"version": "1.7.0"
},
@ -3309,6 +3312,9 @@
}
}
},
"magic-string": {
"version": "0.16.0"
},
"map-obj": {
"version": "1.0.1"
},
@ -3851,6 +3857,20 @@
}
}
},
"rollup-plugin-commonjs": {
"version": "5.0.5",
"dependencies": {
"acorn": {
"version": "4.0.3"
},
"resolve": {
"version": "1.1.7"
}
}
},
"rollup-pluginutils": {
"version": "1.5.2"
},
"rxjs": {
"version": "5.0.0-beta.12"
},
@ -4382,6 +4402,9 @@
}
}
},
"vlq": {
"version": "0.2.1"
},
"vm-browserify": {
"version": "0.0.4"
},

56
npm-shrinkwrap.json generated
View File

@ -110,7 +110,8 @@
},
"amdefine": {
"version": "1.0.1",
"from": "amdefine@>=0.0.4"
"from": "amdefine@1.0.1",
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz"
},
"angular": {
"version": "1.5.0",
@ -2588,7 +2589,7 @@
},
"core-js": {
"version": "2.4.1",
"from": "core-js@2.4.1",
"from": "core-js@latest",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz"
},
"core-util-is": {
@ -2910,6 +2911,11 @@
"from": "esprima@>=2.6.0 <3.0.0",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.1.tgz"
},
"estree-walker": {
"version": "0.2.1",
"from": "estree-walker@>=0.2.1 <0.3.0",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.2.1.tgz"
},
"etag": {
"version": "1.7.0",
"from": "etag@>=1.7.0 <1.8.0",
@ -4262,7 +4268,8 @@
"dependencies": {
"amdefine": {
"version": "1.0.1",
"from": "amdefine@>=0.0.4"
"from": "amdefine@>=0.0.4",
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz"
}
}
}
@ -4559,7 +4566,7 @@
},
"jasmine": {
"version": "2.4.1",
"from": "jasmine@>=2.4.0 <2.5.0",
"from": "jasmine@2.4.1",
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.4.1.tgz",
"dependencies": {
"glob": {
@ -4752,7 +4759,8 @@
"dependencies": {
"amdefine": {
"version": "1.0.1",
"from": "amdefine@>=0.0.4"
"from": "amdefine@>=0.0.4",
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz"
}
}
}
@ -5256,6 +5264,11 @@
}
}
},
"magic-string": {
"version": "0.16.0",
"from": "magic-string@>=0.16.0 <0.17.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.16.0.tgz"
},
"map-obj": {
"version": "1.0.1",
"from": "map-obj@>=1.0.0 <2.0.0",
@ -6116,7 +6129,8 @@
"dependencies": {
"amdefine": {
"version": "1.0.1",
"from": "amdefine@>=0.0.4"
"from": "amdefine@>=0.0.4",
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz"
}
}
},
@ -6137,6 +6151,28 @@
}
}
},
"rollup-plugin-commonjs": {
"version": "5.0.5",
"from": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-5.0.5.tgz",
"resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-5.0.5.tgz",
"dependencies": {
"acorn": {
"version": "4.0.3",
"from": "acorn@>=4.0.1 <5.0.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.3.tgz"
},
"resolve": {
"version": "1.1.7",
"from": "resolve@>=1.1.7 <2.0.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz"
}
}
},
"rollup-pluginutils": {
"version": "1.5.2",
"from": "rollup-pluginutils@>=1.5.1 <2.0.0",
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz"
},
"rxjs": {
"version": "5.0.0-beta.12",
"from": "rxjs@5.0.0-beta.12",
@ -6574,7 +6610,7 @@
},
"through2": {
"version": "0.6.5",
"from": "through2@>=0.6.3 <0.7.0",
"from": "through2@>=0.6.5 <0.7.0",
"resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz",
"dependencies": {
"readable-stream": {
@ -6718,6 +6754,7 @@
"tsickle": {
"version": "0.2.2",
"from": "tsickle@0.2.2",
"resolved": "https://registry.npmjs.org/tsickle/-/tsickle-0.2.2.tgz",
"dependencies": {
"source-map": {
"version": "0.5.6",
@ -6985,6 +7022,11 @@
}
}
},
"vlq": {
"version": "0.2.1",
"from": "vlq@>=0.2.1 <0.3.0",
"resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.1.tgz"
},
"vm-browserify": {
"version": "0.0.4",
"from": "vm-browserify@>=0.0.1 <0.1.0",

View File

@ -71,6 +71,7 @@
"react": "^0.14.0",
"rewire": "^2.3.3",
"rollup": "^0.26.3",
"rollup-plugin-commonjs": "^5.0.5",
"selenium-webdriver": "^2.53.3",
"semver": "^5.1.0",
"source-map": "^0.3.0",

View File

@ -17,5 +17,6 @@ node dist/tools/@angular/tsc-wrapped/src/main -p modules/@angular/core/tsconfig-
node dist/tools/@angular/tsc-wrapped/src/main -p modules/@angular/common/tsconfig-build.json
node dist/tools/@angular/tsc-wrapped/src/main -p modules/@angular/platform-browser/tsconfig-build.json
node dist/tools/@angular/tsc-wrapped/src/main -p modules/@angular/router/tsconfig-build.json
node dist/tools/@angular/tsc-wrapped/src/main -p modules/@angular/forms/tsconfig-build.json
echo 'travis_fold:end:BUILD'