feat(language-service): add services to support editors (#12987)
This commit is contained in:
parent
ef96763fa4
commit
519a324454
2
build.sh
2
build.sh
|
@ -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
|
||||
|
|
|
@ -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/**',
|
||||
|
|
|
@ -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;
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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()]
|
||||
}
|
|
@ -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(); }
|
||||
}
|
|
@ -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>
|
||||
};
|
|
@ -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));
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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); }
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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}))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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', '&', '>', '<', 'ι'); });
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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>';
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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(''); }
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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'); });
|
||||
});
|
|
@ -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;
|
||||
`
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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', '&', '>', '<', 'ι'); });
|
||||
|
||||
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);
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue