diff --git a/source/ast.ts b/source/ast.ts index 9a4a859..2688ed5 100644 --- a/source/ast.ts +++ b/source/ast.ts @@ -3,6 +3,7 @@ import { AccessMember, AccessScope, AccessKeyed, NameExpression, ValueConverter import { Container } from 'aurelia-dependency-injection'; import { Rule, Parser, ParserState, Issue, IssueSeverity } from 'template-lint'; import ts = require('typescript'); +import * as path from 'path'; import { @@ -15,21 +16,28 @@ import { from 'aurelia-templating'; import { ASTAttribute as P5ASTAttribute } from "parse5"; +import { Reflection } from './reflection'; import { AureliaReflection } from './aurelia-reflection'; export class ASTBuilder extends Rule { public root: ASTNode; + public resources: Array<{ name: string, type: ts.DeclarationStatement, kind: string }>; public reportBindingSyntax = true; + private basePath: string; - constructor(protected auReflection?: AureliaReflection) { + + constructor(protected reflection: Reflection, protected auReflection?: AureliaReflection) { super(); this.auReflection = this.auReflection || new AureliaReflection(); + this.resources = []; } - init(parser: Parser) { + init(parser: Parser, filepath: string) { var current = new ASTNode(); this.root = current; + this.basePath = filepath ? path.dirname(filepath) : ""; + parser.on("startTag", (tag, attrs, selfClosing, loc) => { let next = new ASTElementNode(); next.tag = tag; @@ -49,6 +57,37 @@ export class ASTBuilder extends Rule { current.children.push(next); if (!parser.isVoid(tag)) current = next; + + //triage #67 + if (tag === "require") { + var from = attrs.find(x => x.name == "from"); + + if (from == null || from.value == null || from.value == "") + return; + + var lookup = path.normalize(path.join(this.basePath, from.value)); + var source = this.reflection.pathToSource[lookup]; + + if (source == null) + return; + + var customElement = source.statements.find( + (x) => { + return x.kind == ts.SyntaxKind.ClassDeclaration && + (x).name.getText().endsWith("CustomElement"); + }); + + if (!customElement) + return; + + var res = { + kind: "custom-element", + name: this.auReflection.customElementToDash((customElement).name.getText()), + type: customElement + }; + + this.resources.push(res); + }; }); parser.on("endTag", (tag, attrs, selfClosing, loc) => { @@ -207,6 +246,7 @@ export class ASTAttribute { export class ASTElementNode extends ASTNode { public tag: string; public attrs: ASTAttribute[]; + public typeDecl: ts.DeclarationStatement; constructor() { super(); diff --git a/source/aurelia-reflection.ts b/source/aurelia-reflection.ts index 675d3a7..f49388c 100644 --- a/source/aurelia-reflection.ts +++ b/source/aurelia-reflection.ts @@ -31,4 +31,14 @@ export class AureliaReflection { exp = this.bindingLanguage.inspectTextContent(this.resources, text); return exp; } + + toDashCase(value: string) { + return value.replace(/([a-z][A-Z])/g, function (g) { return g[0] + "-" + g[1].toLowerCase(); }); + } + + customElementToDash(value: string) { + if (value.endsWith("CustomElement")) + value = value.substring(0, value.length - "CustomElement".length); + return this.toDashCase(value.charAt(0).toLowerCase() + value.slice(1)); + } } diff --git a/source/rules/binding.ts b/source/rules/binding.ts index eba35bc..a1f900b 100644 --- a/source/rules/binding.ts +++ b/source/rules/binding.ts @@ -38,7 +38,7 @@ export class BindingRule extends ASTBuilder { public restrictedAccess = ["private", "protected"]; constructor( - private reflection: Reflection, + reflection: Reflection, auReflection: AureliaReflection, opt?: { reportBindingSyntax?: boolean, @@ -47,16 +47,15 @@ export class BindingRule extends ASTBuilder { localProvidors?: string[], restrictedAccess?: string[] }) { - - super(auReflection); + super(reflection, auReflection); if (opt) Object.assign(this, opt); } - init(parser: Parser, path?: string) { - super.init(parser); - this.root.context = this.resolveViewModel(path); + init(parser: Parser, filepath?: string) { + super.init(parser, filepath); + this.root.context = this.resolveViewModel(filepath); } finalise(): Issue[] { @@ -94,6 +93,13 @@ export class BindingRule extends ASTBuilder { } private examineElementNode(node: ASTElementNode) { + + var customResource = this.resources.find(x => x.name == node.tag); + + if (customResource != null) { + node.typeDecl = customResource.type; + } + let attrs = node.attrs.sort((a, b) => { var ai = this.localProvidors.indexOf(a.name); var bi = this.localProvidors.indexOf(b.name); @@ -136,7 +142,21 @@ export class BindingRule extends ASTBuilder { switch (instructionName) { case "BehaviorInstruction": { - this.examineBehaviorInstruction(node, instruction); + var behaviorInstruction = instruction; + this.examineBehaviorInstruction(node, behaviorInstruction); + + if (node.typeDecl && node.typeDecl.kind == ts.SyntaxKind.ClassDeclaration && (node.typeDecl).members) { + var members = (node.typeDecl).members; + + if (members.findIndex(x => x.name.getText() == behaviorInstruction.attrName) == -1) { + this.reportIssue({ + message: `cannot find '${behaviorInstruction.attrName}' in type '${node.typeDecl.name.getText()}'`, + severity: IssueSeverity.Error, + line: attr.location.line, + column: attr.location.column + }); + } + } break; } case "ListenerExpression": { diff --git a/spec/ast.spec.ts b/spec/ast.spec.ts index 569d243..edf8608 100644 --- a/spec/ast.spec.ts +++ b/spec/ast.spec.ts @@ -33,7 +33,7 @@ describe("Abstract Syntax Tree", () => { expect(locals[0].type).toEqual(ts.createNode(ts.SyntaxKind.StringKeyword)); }); it("will create correct AST when void present", (done) => { - var builder = new ASTBuilder(); + var builder = new ASTBuilder(new Reflection()); var linter: Linter = new Linter([ builder ]); diff --git a/spec/binding.spec.ts b/spec/binding.spec.ts index f6d8d1d..9aa2923 100644 --- a/spec/binding.spec.ts +++ b/spec/binding.spec.ts @@ -1555,6 +1555,45 @@ describe("Static-Type Binding Tests", () => { }); }); + //#67 + describe("custom elements", () => { + + it("should detect invalid bindable attributes on custom elements", (done) => { + let item = ` + export class ItemCustomElement{ + target:string; + }`; + const viewmodel = ` + export class Foo{ + myname:string; + }`; + const view = ` + `; + const reflection = new Reflection(); + const rule = new BindingRule(reflection, new AureliaReflection()); + const linter = new Linter([rule]); + reflection.add("./foo.ts", viewmodel); + reflection.add("./item", item); + linter.lint(view, "./foo.html") + .then((issues) => { + try { + expect(issues.length).toBe(2); + expect(issues[1].message).toBe("cannot find 'missing' in type 'ItemCustomElement'"); + expect(issues[0].message).toBe("cannot find 'alsoMissing' in type 'Foo'"); + } + catch (err) { + fail(err); + } + finally { + done(); + } + }); + }); + }); + /*it("rejects more than one class in view-model file", (done) => { let viewmodel = ` diff --git a/spec/repeatfor.spec.ts b/spec/repeatfor.spec.ts index c164c88..6239ad5 100644 --- a/spec/repeatfor.spec.ts +++ b/spec/repeatfor.spec.ts @@ -1,12 +1,12 @@ import { Linter, Rule } from 'template-lint'; import { ASTBuilder } from '../source/ast'; +import { Reflection } from '../source/reflection'; describe("RepeatFor Testing", () => { var linter: Linter = new Linter([ - new ASTBuilder() - ]); + new ASTBuilder(new Reflection())]); it("will pass item of items", (done) => { linter.lint('
')