Add file, row and column for ui/builder errors

This commit is contained in:
Panayot Cankov
2015-11-17 18:19:20 +02:00
parent acf8b6e432
commit 8bee3ed2d1
8 changed files with 352 additions and 125 deletions

View File

@@ -0,0 +1,14 @@
<Page shownModally="onShownModally">
<StackLayout>
<!--
At first I was going to put "MenuItem",
as per https://github.com/NativeScript/NativeScript/issues/501,
but then again we may re-introduce "MenuItem" in future and blow this test up.
So here is something unique that has better chance not to appear.
This comment also offsets error's row and column numbers so if you edit,
please do so beyond the unicorn.
-->
<Unicorn backgroundColor="pink" />
<Label text="Modal Page" />
</StackLayout>
</Page>

View File

@@ -835,4 +835,24 @@ export function test_parse_template_property() {
var button = <Button>templateView.getChildAt(0);
TKUnit.assert(button, "Expected the TemplateView's template to create a button child.");
TKUnit.assertEqual(button.text, "Click!", "Expected child Button to have text 'Click!'");
}
}
export function test_ParserError() {
var basePath = "xml-declaration/";
var expectedErrorStart =
"Building UI from XML. @file:///app/" + basePath + "errors/non-existing-element.xml:11:5\n" +
" ↳Module 'ui/unicorn' not found for element 'Unicorn'.\n";
if (global.android) {
expectedErrorStart += " ↳Module \"ui/unicorn\" not found";
} else {
expectedErrorStart += " ↳Failed to find module 'ui/unicorn'";
}
var message;
try {
builder.load(__dirname + "/errors/non-existing-element.xml");
} catch(e) {
message = e.message;
}
TKUnit.assertEqual(message.substr(0, expectedErrorStart.length), expectedErrorStart, "Expected load to throw, and the message to start with specific string");
}

View File

@@ -319,8 +319,8 @@
"apps/tests/xml-declaration/mainPage.ts",
"apps/tests/xml-declaration/mymodule/MyControl.ts",
"apps/tests/xml-declaration/mymodulewithxml/MyControl.ts",
"apps/tests/xml-declaration/xml-declaration-tests.ts",
"apps/tests/xml-declaration/template-builder-tests/template-view.ts",
"apps/tests/xml-declaration/xml-declaration-tests.ts",
"apps/tests/xml-parser-tests/xml-parser-tests.ts",
"apps/transforms/app.ts",
"apps/transforms/main-page.ts",
@@ -658,6 +658,8 @@
"ui/web-view/web-view.android.ts",
"ui/web-view/web-view.d.ts",
"ui/web-view/web-view.ios.ts",
"utils/debug.d.ts",
"utils/debug.ts",
"utils/module-merge.ts",
"utils/number-utils.ts",
"utils/types.d.ts",

View File

@@ -9,6 +9,7 @@ import definition = require("ui/builder");
import page = require("ui/page");
import fileResolverModule = require("file-system/file-name-resolver");
import trace = require("trace");
import debug = require("utils/debug");
var KNOWNCOLLECTIONS = "knownCollections";
@@ -41,7 +42,7 @@ export function parse(value: string | view.Template, context: any): view.View {
}
}
function parseInternal(value: string, context: any): componentBuilder.ComponentModule {
function parseInternal(value: string, context: any, uri?: string): componentBuilder.ComponentModule {
var currentPage: page.Page;
var rootComponentModule: componentBuilder.ComponentModule;
// Temporary collection used for parent scope.
@@ -51,134 +52,148 @@ function parseInternal(value: string, context: any): componentBuilder.ComponentM
var templateBuilder: templateBuilderDef.TemplateBuilder;
var currentPlatformContext: string;
var wrapSource: (e: Error, p: xml.Position) => Error;
if (debug.debug && uri) {
wrapSource = (e: Error, p: xml.Position) => {
var source = new debug.Source(uri, p.line, p.column);
e = new debug.SourceError(e, source, "Building UI from XML.");
return e;
}
} else {
wrapSource = e => e; // no-op identity
}
// Parse the XML.
var xmlParser = new xml.XmlParser((args: xml.ParserEvent) => {
if (args.eventType === xml.ParserEventType.StartElement) {
if (isPlatform(args.elementName)) {
if (currentPlatformContext) {
throw new Error("Already in '" + currentPlatformContext + "' platform context and cannot switch to '" + args.elementName + "' platform! Platform tags cannot be nested.");
try {
if (args.eventType === xml.ParserEventType.StartElement) {
if (isPlatform(args.elementName)) {
if (currentPlatformContext) {
throw new Error("Already in '" + currentPlatformContext + "' platform context and cannot switch to '" + args.elementName + "' platform! Platform tags cannot be nested.");
}
currentPlatformContext = args.elementName;
return;
}
currentPlatformContext = args.elementName;
}
if (args.eventType === xml.ParserEventType.EndElement) {
if (isPlatform(args.elementName)) {
currentPlatformContext = undefined;
return;
}
}
if (currentPlatformContext && !isCurentPlatform(currentPlatformContext)) {
return;
}
}
if (args.eventType === xml.ParserEventType.EndElement) {
if (isPlatform(args.elementName)) {
currentPlatformContext = undefined;
return;
if (templateBuilder) {
var finished = templateBuilder.handleElement(args);
if (finished) {
// Clean-up and continnue
templateBuilder = undefined;
}
else {
// Skip processing untill the template builder finishes his job.
return;
}
}
}
if (currentPlatformContext && !isCurentPlatform(currentPlatformContext)) {
return;
}
if (templateBuilder) {
var finished = templateBuilder.handleElement(args);
if (finished) {
// Clean-up and continnue
templateBuilder = undefined;
}
else {
// Skip processing untill the template builder finishes his job.
return;
}
}
// Get the current parent.
var parent = parents[parents.length - 1];
var complexProperty = complexProperties[complexProperties.length - 1];
// Create component instance from every element declaration.
if (args.eventType === xml.ParserEventType.StartElement) {
if (isComplexProperty(args.elementName)) {
var name = getComplexProperty(args.elementName);
complexProperties.push({
parent: parent,
name: name,
items: [],
});
if (templateBuilderDef.isKnownTemplate(name, parent.exports)) {
templateBuilder = new templateBuilderDef.TemplateBuilder({
context: parent ? getExports(parent.component) : null, // Passing 'context' won't work if you set "codeFile" on the page
// Get the current parent.
var parent = parents[parents.length - 1];
var complexProperty = complexProperties[complexProperties.length - 1];
// Create component instance from every element declaration.
if (args.eventType === xml.ParserEventType.StartElement) {
if (isComplexProperty(args.elementName)) {
var name = getComplexProperty(args.elementName);
complexProperties.push({
parent: parent,
name: name,
elementName: args.elementName,
templateItems: []
items: [],
});
}
} else {
var componentModule: componentBuilder.ComponentModule;
if (args.prefix && args.namespace) {
// Custom components
componentModule = loadCustomComponent(args.namespace, args.elementName, args.attributes, context, currentPage);
if (templateBuilderDef.isKnownTemplate(name, parent.exports)) {
templateBuilder = new templateBuilderDef.TemplateBuilder({
context: parent ? getExports(parent.component) : null, // Passing 'context' won't work if you set "codeFile" on the page
parent: parent,
name: name,
elementName: args.elementName,
templateItems: []
});
}
} else {
// Default components
componentModule = componentBuilder.getComponentModule(args.elementName, args.namespace, args.attributes, context);
}
if (componentModule) {
if (parent) {
if (componentModule.component instanceof view.View) {
if (complexProperty) {
// Add to complex property to component.
addToComplexProperty(parent, complexProperty, componentModule)
var componentModule: componentBuilder.ComponentModule;
if (args.prefix && args.namespace) {
// Custom components
componentModule = loadCustomComponent(args.namespace, args.elementName, args.attributes, context, currentPage);
} else {
// Default components
componentModule = componentBuilder.getComponentModule(args.elementName, args.namespace, args.attributes, context);
}
if (componentModule) {
if (parent) {
if (componentModule.component instanceof view.View) {
if (complexProperty) {
// Add to complex property to component.
addToComplexProperty(parent, complexProperty, componentModule)
} else if ((<any>parent.component)._addChildFromBuilder) {
// Add component to visual tree
(<any>parent.component)._addChildFromBuilder(args.elementName, componentModule.component);
}
} else if (complexProperty) {
// Add component to complex property of parent component.
addToComplexProperty(parent, complexProperty, componentModule);
} else if ((<any>parent.component)._addChildFromBuilder) {
// Add component to visual tree
(<any>parent.component)._addChildFromBuilder(args.elementName, componentModule.component);
}
} else if (complexProperty) {
// Add component to complex property of parent component.
addToComplexProperty(parent, complexProperty, componentModule);
} else if ((<any>parent.component)._addChildFromBuilder) {
(<any>parent.component)._addChildFromBuilder(args.elementName, componentModule.component);
} else if (parents.length === 0) {
// Set root component.
rootComponentModule = componentModule;
if (rootComponentModule && rootComponentModule.component instanceof page.Page) {
currentPage = <page.Page>rootComponentModule.component;
}
}
} else if (parents.length === 0) {
// Set root component.
rootComponentModule = componentModule;
if (rootComponentModule && rootComponentModule.component instanceof page.Page) {
currentPage = <page.Page>rootComponentModule.component;
}
}
// Add the component instance to the parents scope collection.
parents.push(componentModule);
}
}
} else if (args.eventType === xml.ParserEventType.EndElement) {
if (isComplexProperty(args.elementName)) {
if (complexProperty) {
if (parent && (<any>parent.component)._addArrayFromBuilder) {
// If parent is AddArrayFromBuilder call the interface method to populate the array property.
(<any>parent.component)._addArrayFromBuilder(complexProperty.name, complexProperty.items);
complexProperty.items = [];
// Add the component instance to the parents scope collection.
parents.push(componentModule);
}
}
// Remove the last complexProperty from the complexProperties collection (move to the previous complexProperty scope).
complexProperties.pop();
} else {
// Remove the last parent from the parents collection (move to the previous parent scope).
parents.pop();
} else if (args.eventType === xml.ParserEventType.EndElement) {
if (isComplexProperty(args.elementName)) {
if (complexProperty) {
if (parent && (<any>parent.component)._addArrayFromBuilder) {
// If parent is AddArrayFromBuilder call the interface method to populate the array property.
(<any>parent.component)._addArrayFromBuilder(complexProperty.name, complexProperty.items);
complexProperty.items = [];
}
}
// Remove the last complexProperty from the complexProperties collection (move to the previous complexProperty scope).
complexProperties.pop();
} else {
// Remove the last parent from the parents collection (move to the previous parent scope).
parents.pop();
}
}
} catch(e) {
throw wrapSource(e, args.position);
}
}, (e) => {
throw new Error("XML parse error: " + e.message);
}, true);
}, (e, p) => {
throw wrapSource(new Error("XML parse error: " + e.message), p);
}, true);
if (types.isString(value)) {
value = value.replace(/xmlns=("|')http:\/\/((www)|(schemas))\.nativescript\.org\/tns\.xsd\1/, "");
@@ -271,7 +286,7 @@ function loadInternal(fileName: string, context?: any): componentBuilder.Compone
throw new Error("Error loading file " + fileName + " :" + error.message);
}
var text = file.readTextSync(onError);
componentModule = parseInternal(text, context);
componentModule = parseInternal(text, context, fileName);
}
if (componentModule && componentModule.component) {

View File

@@ -5,6 +5,7 @@ import fs = require("file-system");
import bindingBuilder = require("./binding-builder");
import platform = require("platform");
import pages = require("ui/page");
import debug = require("utils/debug");
//the imports below are needed for special property registration
import "ui/layouts/dock-layout";
@@ -60,7 +61,7 @@ export function getComponentModule(elementName: string, namespace: string, attri
// Create instance of the component.
instance = new instanceType();
} catch (ex) {
throw new Error("Cannot create module " + moduleId + ". " + ex + ". StackTrace: " + ex.stack);
throw new debug.ScopeError(ex, "Module '" + moduleId + "' not found for element '" + (namespace ? namespace + ":" : "") + elementName + "'.");
}
if (attributes) {

92
utils/debug.d.ts vendored Normal file
View File

@@ -0,0 +1,92 @@
declare module "utils/debug" {
/**
* A runtime option indicating whether the build has debugging enabled.
*/
export var debug: boolean;
/**
* A class encapsulating information for source code origin.
*/
export class Source {
/**
* Creates a new Source instance by given uri, line and column.
*/
constructor(uri: string, line: number, column: number);
/**
* Gets the URI of the source document;
*/
uri: string;
/**
* Gets the line in the source document.
*/
line: number;
/**
* Gets the position in the source document.
*/
column: number;
/**
* Get the source of an object.
*/
public static get(object: any): Source;
/**
* Set the source of an object.
*/
public static set(object: any, src: Source);
}
/**
* An Error class that provides additional context to an error.
*/
export class ScopeError implements Error {
/**
* Creates a new ScopeError providing addtional context to the child error.
* @param child The child error to extend.
* @param message Additional message to prepend to the child error.
*/
constructor(child: Error, message?: string);
/**
* Gets the child error.
*/
child: Error;
/**
* Gets the error message.
*/
message: string;
/**
* Gets the stack trace.
*/
stack: string;
/**
* Gets the error name.
*/
name: string;
}
/**
* Represents a scope error providing addiot
*/
export class SourceError extends ScopeError {
/**
* Creates a new SourceError by child error, source and optional message.
* @param child The child error to extend.
* @param source The source where the error occured.
* @param message Additonal message to prepend along the source location and the child error's message.
*/
constructor(child: Error, source: Source, message?: string);
/**
* Gets the error source.
*/
source: Source;
}
}

83
utils/debug.ts Normal file
View File

@@ -0,0 +1,83 @@
import { knownFolders } from "file-system"
export var debug = true;
// TODO: Get this from the runtimes...
var applicationRootPath = knownFolders.currentApp().path;
applicationRootPath = applicationRootPath.substr(0, applicationRootPath.length - "app/".length);
export class Source {
private _uri: string;
private _line: number;
private _column: number;
private static _source: symbol = Symbol("source");
private static _appRoot: string;
constructor(uri: string, line: number, column: number) {
if (uri.length > applicationRootPath.length && uri.substr(0, applicationRootPath.length) === applicationRootPath) {
this._uri = "file://" + uri.substr(applicationRootPath.length);
} else {
this._uri = uri;
}
this._line = line;
this._column = column;
}
get uri(): string { return this._uri; }
get line(): number { return this._line; }
get column(): number { return this._column; }
public toString() {
return this._uri + ":" + this._line + ":" + this._column;
}
public static get(object: any): Source {
return object[Source._source];
}
public static set(object: any, src: Source) {
object[Source._source] = src;
}
}
export class ScopeError implements Error {
private _child: Error;
private _message: string;
constructor(child: Error, message?: string) {
if (!child) {
throw new Error("Required child error!");
}
this._child = child;
this._message = message;
}
get child() { return this._child; }
get message() {
if (this._message && this._childMessage) {
// It is a ↳ but the ios fails to show this symbol at the moment.
return this._message + "\n \u21B3" + this._childMessage.replace("\n", "\n ");
}
return this._message || this._childMessage || undefined;
}
get name() { return this.child.name; }
get stack() { return (<any>this.child).stack; }
private get _childMessage(): string {
return this.child.message;
}
public toString() { return "Error: " + this.message; }
}
export class SourceError extends ScopeError {
private _source: Source;
constructor(child: Error, source: Source, message?: string) {
super(child, message ? message + " @" + source + "" : source + "");
this._source = source;
}
get source() { return this._source; }
}

22
xml/xml.d.ts vendored
View File

@@ -100,18 +100,18 @@ declare module "xml" {
*/
class XmlParser {
/**
* Creates a new instance of the XmlParser class.
* @param onEvent The callback to execute when a parser event occurs. The 'event' parameter contains information about the event.
* @param onError The callback to execute when a parser error occurs. The 'error' parameter contains the error.
* @param processNamespaces Specifies whether namespaces should be processed.
*/
constructor(onEvent: (event: ParserEvent) => void, onError?: (error: Error) => void, processNamespaces?: boolean, angularSyntax?: boolean);
/**
* Creates a new instance of the XmlParser class.
* @param onEvent The callback to execute when a parser event occurs. The 'event' parameter contains information about the event.
* @param onError The callback to execute when a parser error occurs. The 'error' parameter contains the error.
* @param processNamespaces Specifies whether namespaces should be processed.
*/
constructor(onEvent: (event: ParserEvent) => void, onError?: (error: Error, position: Position) => void, processNamespaces?: boolean, angularSyntax?: boolean);
/**
* Parses the supplied xml string.
* @param xmlString The string containing the xml to parse.
*/
/**
* Parses the supplied xml string.
* @param xmlString The string containing the xml to parse.
*/
parse(xmlString: string): void;
}
}