mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
Add file, row and column for ui/builder errors
This commit is contained in:
14
apps/tests/xml-declaration/errors/non-existing-element.xml
Normal file
14
apps/tests/xml-declaration/errors/non-existing-element.xml
Normal 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>
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
92
utils/debug.d.ts
vendored
Normal 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
83
utils/debug.ts
Normal 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
22
xml/xml.d.ts
vendored
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user