From 88b607994a148f4af5bffee163e39ce8296750c6 Mon Sep 17 00:00:00 2001 From: The Android Open Source Project Date: Tue, 3 Mar 2009 19:28:42 -0800 Subject: auto import from //depot/cupcake/@135843 --- .../com/android/apicheck/AbstractMethodInfo.java | 24 ++ .../src/com/android/apicheck/ApiCheck.java | 253 ++++++++++++++++++ .../apicheck/src/com/android/apicheck/ApiInfo.java | 81 ++++++ .../src/com/android/apicheck/ClassInfo.java | 282 +++++++++++++++++++++ .../src/com/android/apicheck/ConstructorInfo.java | 147 +++++++++++ .../apicheck/src/com/android/apicheck/Errors.java | 156 ++++++++++++ .../src/com/android/apicheck/FieldInfo.java | 160 ++++++++++++ .../src/com/android/apicheck/MethodInfo.java | 209 +++++++++++++++ .../src/com/android/apicheck/PackageInfo.java | 78 ++++++ .../src/com/android/apicheck/ParameterInfo.java | 35 +++ .../com/android/apicheck/SourcePositionInfo.java | 122 +++++++++ 11 files changed, 1547 insertions(+) create mode 100644 tools/apicheck/src/com/android/apicheck/AbstractMethodInfo.java create mode 100644 tools/apicheck/src/com/android/apicheck/ApiCheck.java create mode 100644 tools/apicheck/src/com/android/apicheck/ApiInfo.java create mode 100644 tools/apicheck/src/com/android/apicheck/ClassInfo.java create mode 100644 tools/apicheck/src/com/android/apicheck/ConstructorInfo.java create mode 100644 tools/apicheck/src/com/android/apicheck/Errors.java create mode 100644 tools/apicheck/src/com/android/apicheck/FieldInfo.java create mode 100644 tools/apicheck/src/com/android/apicheck/MethodInfo.java create mode 100644 tools/apicheck/src/com/android/apicheck/PackageInfo.java create mode 100644 tools/apicheck/src/com/android/apicheck/ParameterInfo.java create mode 100644 tools/apicheck/src/com/android/apicheck/SourcePositionInfo.java (limited to 'tools/apicheck/src/com/android') diff --git a/tools/apicheck/src/com/android/apicheck/AbstractMethodInfo.java b/tools/apicheck/src/com/android/apicheck/AbstractMethodInfo.java new file mode 100644 index 0000000..ca90820 --- /dev/null +++ b/tools/apicheck/src/com/android/apicheck/AbstractMethodInfo.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apicheck; + +public interface AbstractMethodInfo { + + public void addException(String exec); + public void addParameter(ParameterInfo p); + +} diff --git a/tools/apicheck/src/com/android/apicheck/ApiCheck.java b/tools/apicheck/src/com/android/apicheck/ApiCheck.java new file mode 100644 index 0000000..f78117c --- /dev/null +++ b/tools/apicheck/src/com/android/apicheck/ApiCheck.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apicheck; + +import org.xml.sax.*; +import org.xml.sax.helpers.*; +import java.io.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Stack; + +public class ApiCheck { + // parse out and consume the -whatever command line flags + private static ArrayList parseFlags(ArrayList allArgs) { + ArrayList ret = new ArrayList(); + + int i; + for (i = 0; i < allArgs.size(); i++) { + // flags with one value attached + String flag = allArgs.get(i); + if (flag.equals("-error") + || flag.equals("-warning") + || flag.equals("-hide")) { + String[] arg = new String[2]; + arg[0] = flag; + arg[1] = allArgs.get(++i); + ret.add(arg); + } else { + // we've consumed all of the -whatever args, so we're done + break; + } + } + + // i now points to the first non-flag arg; strip what came before + for (; i > 0; i--) { + allArgs.remove(0); + } + return ret; + } + + public static void main(String[] originalArgs) { + // translate to an ArrayList for munging + ArrayList args = new ArrayList(originalArgs.length); + for (String a: originalArgs) { + args.add(a); + } + + ArrayList flags = ApiCheck.parseFlags(args); + for (String[] a: flags) { + if (a[0].equals("-error") || a[0].equals("-warning") + || a[0].equals("-hide")) { + try { + int level = -1; + if (a[0].equals("-error")) { + level = Errors.ERROR; + } + else if (a[0].equals("-warning")) { + level = Errors.WARNING; + } + else if (a[0].equals("-hide")) { + level = Errors.HIDDEN; + } + Errors.setErrorLevel(Integer.parseInt(a[1]), level); + } + catch (NumberFormatException e) { + System.err.println("Bad argument: " + a[0] + " " + a[1]); + System.exit(2); + } + } + } + + String xmlFileName = args.get(0); + String xmlFileNameNew = args.get(1); + XMLReader xmlreader = null; + try { + // parse the XML files into our data structures + xmlreader = XMLReaderFactory.createXMLReader(); + ApiCheck acheck = new ApiCheck(); + MakeHandler handler = acheck.new MakeHandler(); + xmlreader.setContentHandler(handler); + xmlreader.setErrorHandler(handler); + FileReader filereader = new FileReader(xmlFileName); + xmlreader.parse(new InputSource(filereader)); + FileReader filereaderNew = new FileReader(xmlFileNameNew); + xmlreader.parse(new InputSource(filereaderNew)); + + // establish the superclass relationships + handler.getOldApi().resolveSuperclasses(); + handler.getNewApi().resolveSuperclasses(); + + // finally, run the consistency check + handler.getOldApi().isConsistent(handler.getNewApi()); + + } catch (SAXParseException e) { + Errors.error(Errors.PARSE_ERROR, + new SourcePositionInfo(xmlFileName, e.getLineNumber(), 0), + e.getMessage()); + } catch (Exception e) { + e.printStackTrace(); + Errors.error(Errors.PARSE_ERROR, + new SourcePositionInfo(xmlFileName, 0, 0), + e.getMessage()); + } + + Errors.printErrors(); + System.exit(Errors.hadError ? 1 : 0); + } + + private class MakeHandler extends DefaultHandler { + + private Integer mWarningCount; + private ApiInfo mOriginalApi; + private ApiInfo mNewApi; + private boolean mOldApi; + private PackageInfo mCurrentPackage; + private ClassInfo mCurrentClass; + private AbstractMethodInfo mCurrentMethod; + private ConstructorInfo mCurrentConstructor; + private Stack mClassScope = new Stack(); + + + public MakeHandler() { + super(); + mOriginalApi = new ApiInfo(); + mNewApi = new ApiInfo(); + mOldApi = true; + + } + + public void startElement(String uri, String localName, String qName, + Attributes attributes) { + if (qName.equals("package")) { + mCurrentPackage = new PackageInfo(attributes.getValue("name"), + SourcePositionInfo.fromXml(attributes.getValue("source"))); + } else if (qName.equals("class") + || qName.equals("interface")) { + // push the old outer scope for later recovery, then set + // up the new current class object + mClassScope.push(mCurrentClass); + mCurrentClass = new ClassInfo(attributes.getValue("name"), + mCurrentPackage, + attributes.getValue("extends") , + qName.equals("interface"), + Boolean.valueOf( + attributes.getValue("abstract")), + Boolean.valueOf( + attributes.getValue("static")), + Boolean.valueOf( + attributes.getValue("final")), + attributes.getValue("deprecated"), + attributes.getValue("visibility"), + SourcePositionInfo.fromXml(attributes.getValue("source")), + mCurrentClass); + } else if (qName.equals("method")) { + mCurrentMethod = new MethodInfo(attributes.getValue("name"), + attributes.getValue("return") , + Boolean.valueOf( + attributes.getValue("abstract")), + Boolean.valueOf( + attributes.getValue("native")), + Boolean.valueOf( + attributes.getValue("synchronized")), + Boolean.valueOf( + attributes.getValue("static")), + Boolean.valueOf( + attributes.getValue("final")), + attributes.getValue("deprecated"), + attributes.getValue("visibility"), + SourcePositionInfo.fromXml(attributes.getValue("source")), + mCurrentClass); + } else if (qName.equals("constructor")) { + mCurrentMethod = new ConstructorInfo(attributes.getValue("name"), + attributes.getValue("type") , + Boolean.valueOf( + attributes.getValue("static")), + Boolean.valueOf( + attributes.getValue("final")), + attributes.getValue("deprecated"), + attributes.getValue("visibility"), + SourcePositionInfo.fromXml(attributes.getValue("source")), + mCurrentClass); + } else if (qName.equals("field")) { + FieldInfo fInfo = new FieldInfo(attributes.getValue("name"), + attributes.getValue("type") , + Boolean.valueOf( + attributes.getValue("transient")), + Boolean.valueOf( + attributes.getValue("volatile")), + attributes.getValue("value"), + Boolean.valueOf( + attributes.getValue("static")), + Boolean.valueOf( + attributes.getValue("final")), + attributes.getValue("deprecated"), + attributes.getValue("visibility"), + SourcePositionInfo.fromXml(attributes.getValue("source")), + mCurrentClass); + mCurrentClass.addField(fInfo); + } else if (qName.equals("parameter")) { + mCurrentMethod.addParameter(new ParameterInfo(attributes.getValue("type"), + attributes.getValue("name"))); + } else if (qName.equals("exception")) { + mCurrentMethod.addException(attributes.getValue("type")); + } else if (qName.equals("implements")) { + mCurrentClass.addInterface(attributes.getValue("name")); + } + } + public void endElement(String uri, String localName, String qName) { + if (qName.equals("method")) { + mCurrentClass.addMethod((MethodInfo) mCurrentMethod); + } else if (qName.equals("constructor")) { + mCurrentClass.addConstructor((ConstructorInfo) mCurrentMethod); + } else if (qName.equals("class") + || qName.equals("interface")) { + mCurrentPackage.addClass(mCurrentClass); + mCurrentClass = mClassScope.pop(); + } else if (qName.equals("package")){ + if (mOldApi) { + mOriginalApi.addPackage(mCurrentPackage); + } else { + mNewApi.addPackage(mCurrentPackage); + } + } + } + public void endDocument() { + mOldApi = !mOldApi; + } + + public ApiInfo getOldApi() { + return mOriginalApi; + } + + public ApiInfo getNewApi() { + return mNewApi; + } + + + } +} diff --git a/tools/apicheck/src/com/android/apicheck/ApiInfo.java b/tools/apicheck/src/com/android/apicheck/ApiInfo.java new file mode 100644 index 0000000..01d8f9e --- /dev/null +++ b/tools/apicheck/src/com/android/apicheck/ApiInfo.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apicheck; +import java.util.*; + +public class ApiInfo { + + private HashMap mPackages; + private HashMap mAllClasses; + + public ApiInfo() { + mPackages = new HashMap(); + mAllClasses = new HashMap(); + } + + public boolean isConsistent(ApiInfo otherApi) { + boolean consistent = true; + for (PackageInfo pInfo : mPackages.values()) { + if (otherApi.getPackages().containsKey(pInfo.name())) { + if (!pInfo.isConsistent(otherApi.getPackages().get(pInfo.name()))) { + consistent = false; + } + } else { + Errors.error(Errors.REMOVED_PACKAGE, pInfo.position(), + "Removed package " + pInfo.name()); + consistent = false; + } + } + for (PackageInfo pInfo : otherApi.mPackages.values()) { + if (!pInfo.isInBoth()) { + Errors.error(Errors.ADDED_PACKAGE, pInfo.position(), + "Added package " + pInfo.name()); + consistent = false; + } + } + return consistent; + } + + public HashMap getPackages() { + return mPackages; + } + + public void addPackage(PackageInfo pInfo) { + // track the set of organized packages in the API + mPackages.put(pInfo.name(), pInfo); + + // accumulate a direct map of all the classes in the API + for (ClassInfo cl: pInfo.allClasses().values()) { + mAllClasses.put(cl.qualifiedName(), cl); + } + } + + public void resolveSuperclasses() { + for (ClassInfo cl: mAllClasses.values()) { + // java.lang.Object has no superclass + if (!cl.qualifiedName().equals("java.lang.Object")) { + String scName = cl.superclassName(); + if (scName == null) { + scName = "java.lang.Object"; + } + + ClassInfo superclass = mAllClasses.get(scName); + cl.setSuperClass(superclass); + } + } + } +} diff --git a/tools/apicheck/src/com/android/apicheck/ClassInfo.java b/tools/apicheck/src/com/android/apicheck/ClassInfo.java new file mode 100644 index 0000000..4bbf78b --- /dev/null +++ b/tools/apicheck/src/com/android/apicheck/ClassInfo.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apicheck; +import java.util.*; + +public class ClassInfo { + private String mName; + private String mSuperClassName; + private boolean mIsInterface; + private boolean mIsAbstract; + private boolean mIsStatic; + private boolean mIsFinal; + private String mDeprecated; + private String mScope; + private List mInterfaces; + private HashMap mMethods; + private HashMap mFields; + private HashMap mConstructors; + private boolean mExistsInBoth; + private PackageInfo mPackage; + private SourcePositionInfo mSourcePosition; + private ClassInfo mSuperClass; + private ClassInfo mParentClass; + + public ClassInfo(String name, PackageInfo pack, String superClass, boolean isInterface, + boolean isAbstract, boolean isStatic, boolean isFinal, String deprecated, + String visibility, SourcePositionInfo source, ClassInfo parent) { + mName = name; + mPackage = pack; + mSuperClassName = superClass; + mIsInterface = isInterface; + mIsAbstract = isAbstract; + mIsStatic = isStatic; + mIsFinal = isFinal; + mDeprecated = deprecated; + mScope = visibility; + mInterfaces = new ArrayList(); + mMethods = new HashMap(); + mFields = new HashMap(); + mConstructors = new HashMap(); + mExistsInBoth = false; + mSourcePosition = source; + mParentClass = parent; + } + + public String name() { + return mName; + } + + public String qualifiedName() { + String parentQName = (mParentClass != null) + ? (mParentClass.qualifiedName() + ".") + : ""; + return mPackage.name() + "." + parentQName + name(); + } + + public String superclassName() { + return mSuperClassName; + } + + public SourcePositionInfo position() { + return mSourcePosition; + } + + public boolean isInterface() { + return mIsInterface; + } + + public boolean isFinal() { + return mIsFinal; + } + + // Find a superclass implementation of the given method. Looking at our superclass + // instead of at 'this' is unusual, but it fits the point-of-call demands well. + public MethodInfo overriddenMethod(MethodInfo candidate) { + if (mSuperClass == null) { + return null; + } + + // does our immediate superclass have it? + ClassInfo sup = mSuperClass; + for (MethodInfo mi : sup.mMethods.values()) { + if (mi.matches(candidate)) { + // found it + return mi; + } + } + + // no, so recurse + if (sup.mSuperClass != null) { + return mSuperClass.overriddenMethod(candidate); + } + + // no parent, so we just don't have it + return null; + } + + public boolean isConsistent(ClassInfo cl) { + cl.mExistsInBoth = true; + mExistsInBoth = true; + boolean consistent = true; + + if (isInterface() != cl.isInterface()) { + Errors.error(Errors.CHANGED_CLASS, cl.position(), + "Class " + cl.qualifiedName() + + " changed class/interface declaration"); + consistent = false; + } + for (String iface : mInterfaces) { + if (!cl.mInterfaces.contains(iface)) { + Errors.error(Errors.REMOVED_INTERFACE, cl.position(), + "Class " + qualifiedName() + " no longer implements " + iface); + } + } + for (String iface : cl.mInterfaces) { + if (!mInterfaces.contains(iface)) { + Errors.error(Errors.ADDED_INTERFACE, cl.position(), + "Added interface " + iface + " to class " + + qualifiedName()); + consistent = false; + } + } + + for (MethodInfo mInfo : mMethods.values()) { + if (cl.mMethods.containsKey(mInfo.getHashableName())) { + if (!mInfo.isConsistent(cl.mMethods.get(mInfo.getHashableName()))) { + consistent = false; + } + } else { + /* This class formerly provided this method directly, and now does not. + * Check our ancestry to see if there's an inherited version that still + * fulfills the API requirement. + */ + MethodInfo mi = mInfo.containingClass().overriddenMethod(mInfo); + if (mi == null) { + Errors.error(Errors.REMOVED_METHOD, mInfo.position(), + "Removed public method " + mInfo.qualifiedName()); + consistent = false; + } + } + } + for (MethodInfo mInfo : cl.mMethods.values()) { + if (!mInfo.isInBoth()) { + /* Similarly to the above, do not fail if this "new" method is + * really an override of an existing superclass method. + */ + MethodInfo mi = mInfo.containingClass().overriddenMethod(mInfo); + if (mi == null) { + Errors.error(Errors.ADDED_METHOD, mInfo.position(), + "Added public method " + mInfo.qualifiedName()); + consistent = false; + } + } + } + + for (ConstructorInfo mInfo : mConstructors.values()) { + if (cl.mConstructors.containsKey(mInfo.getHashableName())) { + if (!mInfo.isConsistent(cl.mConstructors.get(mInfo.getHashableName()))) { + consistent = false; + } + } else { + Errors.error(Errors.REMOVED_METHOD, mInfo.position(), + "Removed public constructor " + mInfo.prettySignature()); + consistent = false; + } + } + for (ConstructorInfo mInfo : cl.mConstructors.values()) { + if (!mInfo.isInBoth()) { + Errors.error(Errors.ADDED_METHOD, mInfo.position(), + "Added public constructor " + mInfo.prettySignature()); + consistent = false; + } + } + + for (FieldInfo mInfo : mFields.values()) { + if (cl.mFields.containsKey(mInfo.qualifiedName())) { + if (!mInfo.isConsistent(cl.mFields.get(mInfo.qualifiedName()))) { + consistent = false; + } + } else { + Errors.error(Errors.REMOVED_FIELD, mInfo.position(), + "Removed field " + mInfo.qualifiedName()); + consistent = false; + } + } + for (FieldInfo mInfo : cl.mFields.values()) { + if (!mInfo.isInBoth()) { + Errors.error(Errors.ADDED_FIELD, mInfo.position(), + "Added public field " + mInfo.qualifiedName()); + consistent = false; + } + } + + if (mIsAbstract != cl.mIsAbstract) { + consistent = false; + Errors.error(Errors.CHANGED_ABSTRACT, cl.position(), + "Class " + cl.qualifiedName() + " changed abstract qualifier"); + } + + if (mIsFinal != cl.mIsFinal) { + consistent = false; + Errors.error(Errors.CHANGED_FINAL, cl.position(), + "Class " + cl.qualifiedName() + " changed final qualifier"); + } + + if (mIsStatic != cl.mIsStatic) { + consistent = false; + Errors.error(Errors.CHANGED_STATIC, cl.position(), + "Class " + cl.qualifiedName() + " changed static qualifier"); + } + + if (!mScope.equals(cl.mScope)) { + consistent = false; + Errors.error(Errors.CHANGED_SCOPE, cl.position(), + "Class " + cl.qualifiedName() + " scope changed from " + + mScope + " to " + cl.mScope); + } + + if (!mDeprecated.equals(cl.mDeprecated)) { + consistent = false; + Errors.error(Errors.CHANGED_DEPRECATED, cl.position(), + "Class " + cl.qualifiedName() + " has changed deprecation state"); + } + + if (mSuperClassName != null) { + if (cl.mSuperClassName == null || !mSuperClassName.equals(cl.mSuperClassName)) { + consistent = false; + Errors.error(Errors.CHANGED_SUPERCLASS, cl.position(), + "Class " + qualifiedName() + " superclass changed from " + + mSuperClassName + " to " + cl.mSuperClassName); + } + } else if (cl.mSuperClassName != null) { + consistent = false; + Errors.error(Errors.CHANGED_SUPERCLASS, cl.position(), + "Class " + qualifiedName() + " superclass changed from " + + "null to " + cl.mSuperClassName); + } + + return consistent; + } + + public void addInterface(String name) { + mInterfaces.add(name); + } + + public void addMethod(MethodInfo mInfo) { + mMethods.put(mInfo.getHashableName(), mInfo); + } + + public void addConstructor(ConstructorInfo cInfo) { + mConstructors.put(cInfo.getHashableName(), cInfo); + + } + + public void addField(FieldInfo fInfo) { + mFields.put(fInfo.qualifiedName(), fInfo); + + } + + public void setSuperClass(ClassInfo superclass) { + mSuperClass = superclass; + } + + public boolean isInBoth() { + return mExistsInBoth; + } + +} diff --git a/tools/apicheck/src/com/android/apicheck/ConstructorInfo.java b/tools/apicheck/src/com/android/apicheck/ConstructorInfo.java new file mode 100644 index 0000000..57d7617 --- /dev/null +++ b/tools/apicheck/src/com/android/apicheck/ConstructorInfo.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apicheck; +import java.util.*; + +public class ConstructorInfo implements AbstractMethodInfo { + + private String mName; + private String mType; + private boolean mIsStatic; + private boolean mIsFinal; + private String mDeprecated; + private String mScope; + private List mExceptions; + private List mParameters; + private boolean mExistsInBoth; + private SourcePositionInfo mSourcePosition; + private ClassInfo mClass; + + public ConstructorInfo(String name, String type, boolean isStatic, boolean isFinal, + String deprecated, String scope, SourcePositionInfo pos, ClassInfo clazz) { + mName = name; + mType = type; + mIsStatic = isStatic; + mIsFinal = isFinal; + mDeprecated= deprecated; + mScope = scope; + mExistsInBoth = false; + mExceptions = new ArrayList(); + mParameters = new ArrayList(); + mSourcePosition = pos; + mClass = clazz; + } + + public void addParameter(ParameterInfo pInfo) { + mParameters.add(pInfo); + } + + public void addException(String exec) { + mExceptions.add(exec); + } + + public String getHashableName() { + String returnString = qualifiedName(); + for (ParameterInfo pInfo : mParameters) { + returnString += ":" + pInfo.getType(); + } + return returnString; + } + + public boolean isInBoth() { + return mExistsInBoth; + } + + public SourcePositionInfo position() { + return mSourcePosition; + } + + public String name() { + return mName; + } + + public String qualifiedName() { + String baseName = (mClass != null) + ? (mClass.qualifiedName() + ".") + : ""; + return baseName + name(); + } + + public String prettySignature() { + String params = ""; + for (ParameterInfo pInfo : mParameters) { + if (params.length() > 0) { + params += ", "; + } + params += pInfo.getType(); + } + return qualifiedName() + '(' + params + ')'; + } + + public boolean isConsistent(ConstructorInfo mInfo) { + mInfo.mExistsInBoth = true; + mExistsInBoth = true; + boolean consistent = true; + + if (mIsFinal != mInfo.mIsFinal) { + consistent = false; + Errors.error(Errors.CHANGED_FINAL, mInfo.position(), + "Constructor " + mInfo.qualifiedName() + " has changed 'final' qualifier"); + } + + if (mIsStatic != mInfo.mIsStatic) { + consistent = false; + Errors.error(Errors.CHANGED_FINAL, mInfo.position(), + "Constructor " + mInfo.qualifiedName() + " has changed 'static' qualifier"); + } + + if (!mScope.equals(mInfo.mScope)) { + consistent = false; + Errors.error(Errors.CHANGED_SCOPE, mInfo.position(), + "Constructor " + mInfo.qualifiedName() + " changed scope from " + + mScope + " to " + mInfo.mScope); + } + + if (!mDeprecated.equals(mInfo.mDeprecated)) { + consistent = false; + Errors.error(Errors.CHANGED_DEPRECATED, mInfo.position(), + "Constructor " + mInfo.qualifiedName() + " has changed deprecation state"); + } + + for (String exec : mExceptions) { + if (!mInfo.mExceptions.contains(exec)) { + Errors.error(Errors.CHANGED_THROWS, mInfo.position(), + "Constructor " + mInfo.qualifiedName() + " no longer throws exception " + + exec); + consistent = false; + } + } + + for (String exec : mInfo.mExceptions) { + if (!mExceptions.contains(exec)) { + Errors.error(Errors.CHANGED_THROWS, mInfo.position(), + "Constructor " + mInfo.qualifiedName() + " added thrown exception " + + exec); + consistent = false; + } + } + + return consistent; + } + + +} diff --git a/tools/apicheck/src/com/android/apicheck/Errors.java b/tools/apicheck/src/com/android/apicheck/Errors.java new file mode 100644 index 0000000..d7013e3 --- /dev/null +++ b/tools/apicheck/src/com/android/apicheck/Errors.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apicheck; + +import java.lang.Comparable; +import java.util.TreeSet; + +public class Errors +{ + public static boolean hadError = false; + private static boolean warningsAreErrors = false; + private static TreeSet allErrors = new TreeSet(); + + private static class Message implements Comparable { + SourcePositionInfo pos; + String msg; + + Message(SourcePositionInfo p, String m) { + pos = p; + msg = m; + } + + public int compareTo(Object o) { + Message that = (Message)o; + int r = this.pos.compareTo(that.pos); + if (r != 0) return r; + return this.msg.compareTo(that.msg); + } + + public String toString() { + return this.pos.toString() + this.msg; + } + } + + public static void error(Error error, SourcePositionInfo where, String text) { + if (error.level == HIDDEN) { + return; + } + + String which = (!warningsAreErrors && error.level == WARNING) ? " warning " : " error "; + String message = which + error.code + ": " + text; + + if (where == null) { + where = new SourcePositionInfo("unknown", 0, 0); + } + + allErrors.add(new Message(where, message)); + + if (error.level == ERROR || (warningsAreErrors && error.level == WARNING)) { + hadError = true; + } + } + + public static void printErrors() { + for (Message m: allErrors) { + System.err.println(m.toString()); + } + } + + public static int HIDDEN = 0; + public static int WARNING = 1; + public static int ERROR = 2; + + public static void setWarningsAreErrors(boolean val) { + warningsAreErrors = val; + } + + public static class Error { + public int code; + public int level; + + public Error(int code, int level) + { + this.code = code; + this.level = level; + } + } + + public static Error PARSE_ERROR = new Error(1, ERROR); + public static Error ADDED_PACKAGE = new Error(2, WARNING); + public static Error ADDED_CLASS = new Error(3, WARNING); + public static Error ADDED_METHOD = new Error(4, WARNING); + public static Error ADDED_FIELD = new Error(5, WARNING); + public static Error ADDED_INTERFACE = new Error(6, WARNING); + public static Error REMOVED_PACKAGE = new Error(7, WARNING); + public static Error REMOVED_CLASS = new Error(8, WARNING); + public static Error REMOVED_METHOD = new Error(9, WARNING); + public static Error REMOVED_FIELD = new Error(10, WARNING); + public static Error REMOVED_INTERFACE = new Error(11, WARNING); + public static Error CHANGED_STATIC = new Error(12, WARNING); + public static Error CHANGED_FINAL = new Error(13, WARNING); + public static Error CHANGED_TRANSIENT = new Error(14, WARNING); + public static Error CHANGED_VOLATILE = new Error(15, WARNING); + public static Error CHANGED_TYPE = new Error(16, WARNING); + public static Error CHANGED_VALUE = new Error(17, WARNING); + public static Error CHANGED_SUPERCLASS = new Error(18, WARNING); + public static Error CHANGED_SCOPE = new Error(19, WARNING); + public static Error CHANGED_ABSTRACT = new Error(20, WARNING); + public static Error CHANGED_THROWS = new Error(21, WARNING); + public static Error CHANGED_NATIVE = new Error(22, HIDDEN); + public static Error CHANGED_CLASS = new Error(23, WARNING); + public static Error CHANGED_DEPRECATED = new Error(24, WARNING); + public static Error CHANGED_SYNCHRONIZED = new Error(25, ERROR); + + public static Error[] ERRORS = { + PARSE_ERROR, + ADDED_PACKAGE, + ADDED_CLASS, + ADDED_METHOD, + ADDED_FIELD, + ADDED_INTERFACE, + REMOVED_PACKAGE, + REMOVED_CLASS, + REMOVED_METHOD, + REMOVED_FIELD, + REMOVED_INTERFACE, + CHANGED_STATIC, + CHANGED_FINAL, + CHANGED_TRANSIENT, + CHANGED_VOLATILE, + CHANGED_TYPE, + CHANGED_VALUE, + CHANGED_SUPERCLASS, + CHANGED_SCOPE, + CHANGED_ABSTRACT, + CHANGED_THROWS, + CHANGED_NATIVE, + CHANGED_CLASS, + CHANGED_DEPRECATED, + CHANGED_SYNCHRONIZED, + }; + + public static boolean setErrorLevel(int code, int level) { + for (Error e: ERRORS) { + if (e.code == code) { + e.level = level; + return true; + } + } + return false; + } +} diff --git a/tools/apicheck/src/com/android/apicheck/FieldInfo.java b/tools/apicheck/src/com/android/apicheck/FieldInfo.java new file mode 100644 index 0000000..d80d9f6 --- /dev/null +++ b/tools/apicheck/src/com/android/apicheck/FieldInfo.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apicheck; + +public class FieldInfo { + + private String mName; + private String mType; + private boolean mIsTransient; + private boolean mIsVolatile; + private String mValue; + private boolean mIsStatic; + private boolean mIsFinal; + private String mDeprecated; + private String mScope; + private boolean mExistsInBoth; + private SourcePositionInfo mSourcePosition; + private ClassInfo mClass; + + public FieldInfo (String name, String type, boolean isTransient, boolean isVolatile, + String value, boolean isStatic, boolean isFinal, String deprecated, + String scope, SourcePositionInfo source, ClassInfo parent) { + mName = name; + mType = type; + mIsTransient = isTransient; + mIsVolatile = isVolatile; + mValue = value; + mIsStatic = isStatic; + mIsFinal = isFinal; + mDeprecated = deprecated; + mScope = scope; + mExistsInBoth = false; + mSourcePosition = source; + mClass = parent; + } + + public boolean isInBoth() { + return mExistsInBoth; + } + public SourcePositionInfo position() { + return mSourcePosition; + } + + public String name() { + return mName; + } + + public String qualifiedName() { + String parentQName = (mClass != null) + ? (mClass.qualifiedName() + ".") + : ""; + return parentQName + name(); + } + + // Check the declared value with a typed comparison, not a string comparison, + // to accommodate toolchains with different fp -> string conversions. + public boolean valueEquals(FieldInfo other) { + // Type mismatch means nonequal, as does a null/non-null mismatch + if (!mType.equals(other.mType) + || ((mValue == null) != (other.mValue == null))) { + return false; + } + + // Null values are considered equal + if (mValue == null) { + return true; + } + + // Floating point gets an implementation-type comparison; all others just use the string + // If float/double parse fails, fall back to string comparison -- it means that it's a + // canonical droiddoc-generated constant expression that represents a NaN. + try { + if (mType.equals("float")) { + float val = Float.parseFloat(mValue); + float otherVal = Float.parseFloat(other.mValue); + return (val == otherVal); + } else if (mType.equals("double")) { + double val = Double.parseDouble(mValue); + double otherVal = Double.parseDouble(other.mValue); + return (val == otherVal); + } + } catch (NumberFormatException e) { + // fall through + } + + return mValue.equals(other.mValue); + } + + public boolean isConsistent(FieldInfo fInfo) { + fInfo.mExistsInBoth = true; + mExistsInBoth = true; + boolean consistent = true; + if (!mType.equals(fInfo.mType)) { + Errors.error(Errors.CHANGED_TYPE, fInfo.position(), + "Field " + fInfo.qualifiedName() + " has changed type"); + consistent = false; + } + + if (!this.valueEquals(fInfo)) { + Errors.error(Errors.CHANGED_VALUE, fInfo.position(), + "Field " + fInfo.qualifiedName() + " has changed value from " + + mValue + " to " + fInfo.mValue); + consistent = false; + } + + if (!mScope.equals(fInfo.mScope)) { + Errors.error(Errors.CHANGED_SCOPE, fInfo.position(), + "Method " + fInfo.qualifiedName() + " changed scope from " + + mScope + " to " + fInfo.mScope); + consistent = false; + } + + if (mIsStatic != fInfo.mIsStatic) { + Errors.error(Errors.CHANGED_STATIC, fInfo.position(), + "Field " + fInfo.qualifiedName() + " has changed 'static' qualifier"); + consistent = false; + } + + if (mIsFinal != fInfo.mIsFinal) { + Errors.error(Errors.CHANGED_FINAL, fInfo.position(), + "Field " + fInfo.qualifiedName() + " has changed 'final' qualifier"); + consistent = false; + } + + if (mIsTransient != fInfo.mIsTransient) { + Errors.error(Errors.CHANGED_TRANSIENT, fInfo.position(), + "Field " + fInfo.qualifiedName() + " has changed 'transient' qualifier"); + consistent = false; + } + + if (mIsVolatile != fInfo.mIsVolatile) { + Errors.error(Errors.CHANGED_VOLATILE, fInfo.position(), + "Field " + fInfo.qualifiedName() + " has changed 'volatile' qualifier"); + consistent = false; + } + + if (!mDeprecated.equals(fInfo.mDeprecated)) { + Errors.error(Errors.CHANGED_DEPRECATED, fInfo.position(), + "Field " + fInfo.qualifiedName() + " has changed deprecation state"); + consistent = false; + } + + return consistent; + } + +} diff --git a/tools/apicheck/src/com/android/apicheck/MethodInfo.java b/tools/apicheck/src/com/android/apicheck/MethodInfo.java new file mode 100644 index 0000000..86e20de --- /dev/null +++ b/tools/apicheck/src/com/android/apicheck/MethodInfo.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apicheck; +import java.util.*; + +public class MethodInfo implements AbstractMethodInfo { + + private String mName; + private String mReturn; + private boolean mIsAbstract; + private boolean mIsNative; + private boolean mIsSynchronized; + private boolean mIsStatic; + private boolean mIsFinal; + private String mDeprecated; + private String mScope; + private boolean mExistsInBoth; + private List mParameters; + private List mExceptions; + private SourcePositionInfo mSourcePosition; + private ClassInfo mClass; + + public MethodInfo (String name, String returnType, boolean isAbstract, boolean isNative, + boolean isSynchronized, boolean isStatic, boolean isFinal, String deprecated + , String scope, SourcePositionInfo source, ClassInfo parent) { + + mName = name; + mReturn = returnType; + mIsAbstract = isAbstract; + mIsNative = isNative; + mIsSynchronized = isSynchronized; + mIsStatic = isStatic; + mIsFinal = isFinal; + mDeprecated = deprecated; + mScope = scope; + mParameters = new ArrayList(); + mExceptions = new ArrayList(); + mExistsInBoth = false; + mSourcePosition = source; + mClass = parent; + } + + + public String name() { + return mName; + } + + public String qualifiedName() { + String parentQName = (mClass != null) + ? (mClass.qualifiedName() + ".") + : ""; + return parentQName + name(); + } + + public String prettySignature() { + String params = ""; + for (ParameterInfo pInfo : mParameters) { + if (params.length() > 0) { + params += ", "; + } + params += pInfo.getType(); + } + return qualifiedName() + '(' + params + ')'; + } + + public SourcePositionInfo position() { + return mSourcePosition; + } + + public ClassInfo containingClass() { + return mClass; + } + + public boolean matches(MethodInfo other) { + return getSignature().equals(other.getSignature()); + } + + public boolean isConsistent(MethodInfo mInfo) { + mInfo.mExistsInBoth = true; + mExistsInBoth = true; + boolean consistent = true; + if (!mReturn.equals(mInfo.mReturn)) { + consistent = false; + Errors.error(Errors.CHANGED_TYPE, mInfo.position(), + "Method " + mInfo.qualifiedName() + " has changed return type from " + + mReturn + " to " + mInfo.mReturn); + } + + if (mIsAbstract != mInfo.mIsAbstract) { + consistent = false; + Errors.error(Errors.CHANGED_ABSTRACT, mInfo.position(), + "Method " + mInfo.qualifiedName() + " has changed 'abstract' qualifier"); + } + + if (mIsNative != mInfo.mIsNative) { + consistent = false; + Errors.error(Errors.CHANGED_NATIVE, mInfo.position(), + "Method " + mInfo.qualifiedName() + " has changed 'native' qualifier"); + } + + if (mIsFinal != mInfo.mIsFinal) { + // Compiler-generated methods vary in their 'final' qual between versions of + // the compiler, so this check needs to be quite narrow. A change in 'final' + // status of a method is only relevant if (a) the method is not declared 'static' + // and (b) the method's class is not itself 'final'. + if (!mIsStatic) { + if ((mClass == null) || (!mClass.isFinal())) { + consistent = false; + Errors.error(Errors.CHANGED_FINAL, mInfo.position(), + "Method " + mInfo.qualifiedName() + " has changed 'final' qualifier"); + } + } + } + + if (mIsStatic != mInfo.mIsStatic) { + consistent = false; + Errors.error(Errors.CHANGED_STATIC, mInfo.position(), + "Method " + mInfo.qualifiedName() + " has changed 'static' qualifier"); + } + + if (!mScope.equals(mInfo.mScope)) { + consistent = false; + Errors.error(Errors.CHANGED_SCOPE, mInfo.position(), + "Method " + mInfo.qualifiedName() + " changed scope from " + + mScope + " to " + mInfo.mScope); + } + + if (!mDeprecated.equals(mInfo.mDeprecated)) { + Errors.error(Errors.CHANGED_DEPRECATED, mInfo.position(), + "Method " + mInfo.qualifiedName() + " has changed deprecation state"); + consistent = false; + } + + if (mIsSynchronized != mInfo.mIsSynchronized) { + Errors.error(Errors.CHANGED_SYNCHRONIZED, mInfo.position(), + "Method " + mInfo.qualifiedName() + " has changed 'synchronized' qualifier from " + mIsSynchronized + " to " + mInfo.mIsSynchronized); + consistent = false; + } + + for (String exec : mExceptions) { + if (!mInfo.mExceptions.contains(exec)) { + // exclude 'throws' changes to finalize() overrides with no arguments + if (!name().equals("finalize") || (mParameters.size() > 0)) { + Errors.error(Errors.CHANGED_THROWS, mInfo.position(), + "Method " + mInfo.qualifiedName() + " no longer throws exception " + + exec); + consistent = false; + } + } + } + + for (String exec : mInfo.mExceptions) { + // exclude 'throws' changes to finalize() overrides with no arguments + if (!mExceptions.contains(exec)) { + if (!name().equals("finalize") || (mParameters.size() > 0)) { + Errors.error(Errors.CHANGED_THROWS, mInfo.position(), + "Method " + mInfo.qualifiedName() + " added thrown exception " + + exec); + consistent = false; + } + } + } + + return consistent; + } + + public void addParameter(ParameterInfo pInfo) { + mParameters.add(pInfo); + } + + public void addException(String exc) { + mExceptions.add(exc); + } + + public String getParameterHash() { + String hash = ""; + for (ParameterInfo pInfo : mParameters) { + hash += ":" + pInfo.getType(); + } + return hash; + } + + public String getHashableName() { + return qualifiedName() + getParameterHash(); + } + + public String getSignature() { + return name() + getParameterHash(); + } + + public boolean isInBoth() { + return mExistsInBoth; + } + +} diff --git a/tools/apicheck/src/com/android/apicheck/PackageInfo.java b/tools/apicheck/src/com/android/apicheck/PackageInfo.java new file mode 100644 index 0000000..2262f21 --- /dev/null +++ b/tools/apicheck/src/com/android/apicheck/PackageInfo.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apicheck; +import java.util.*; + +public class PackageInfo { + private String mName; + private HashMap mClasses; + private boolean mExistsInBoth; + private SourcePositionInfo mPosition; + + public PackageInfo(String name, SourcePositionInfo position) { + mName = name; + mClasses = new HashMap(); + mExistsInBoth = false; + mPosition = position; + } + + public void addClass(ClassInfo cl) { + mClasses.put(cl.name() , cl); + } + + public HashMap allClasses() { + return mClasses; + } + + public String name() { + return mName; + } + + public SourcePositionInfo position() { + return mPosition; + } + + public boolean isConsistent(PackageInfo pInfo) { + mExistsInBoth = true; + pInfo.mExistsInBoth = true; + boolean consistent = true; + for (ClassInfo cInfo : mClasses.values()) { + if (pInfo.mClasses.containsKey(cInfo.name())) { + if (!cInfo.isConsistent(pInfo.mClasses.get(cInfo.name()))) { + consistent = false; + } + } else { + Errors.error(Errors.REMOVED_CLASS, cInfo.position(), + "Removed public class " + cInfo.qualifiedName()); + consistent = false; + } + } + for (ClassInfo cInfo : pInfo.mClasses.values()) { + if (!cInfo.isInBoth()) { + Errors.error(Errors.ADDED_CLASS, cInfo.position(), + "Added class " + cInfo.name() + " to package " + + pInfo.name()); + consistent = false; + } + } + return consistent; + } + + public boolean isInBoth() { + return mExistsInBoth; + } +} diff --git a/tools/apicheck/src/com/android/apicheck/ParameterInfo.java b/tools/apicheck/src/com/android/apicheck/ParameterInfo.java new file mode 100644 index 0000000..5788814 --- /dev/null +++ b/tools/apicheck/src/com/android/apicheck/ParameterInfo.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apicheck; + +public class ParameterInfo { + private String mType; + private String mName; + + public ParameterInfo(String type, String name) { + mType = type; + mName = name; + } + + public String getType() { + return mType; + } + + public String getName() { + return mName; + } +} diff --git a/tools/apicheck/src/com/android/apicheck/SourcePositionInfo.java b/tools/apicheck/src/com/android/apicheck/SourcePositionInfo.java new file mode 100644 index 0000000..477c1d3 --- /dev/null +++ b/tools/apicheck/src/com/android/apicheck/SourcePositionInfo.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apicheck; + +import java.lang.Comparable; + +public class SourcePositionInfo implements Comparable +{ + public SourcePositionInfo() { + this.file = ""; + this.line = 0; + this.column = 0; + } + + public SourcePositionInfo(String file, int line, int column) + { + this.file = file; + this.line = line; + this.column = column; + } + + public SourcePositionInfo(SourcePositionInfo that) + { + this.file = that.file; + this.line = that.line; + this.column = that.column; + } + + /** + * Given this position and str which occurs at that position, as well as str an index into str, + * find the SourcePositionInfo. + * + * @throw StringIndexOutOfBoundsException if index > str.length() + */ + public static SourcePositionInfo add(SourcePositionInfo that, String str, int index) + { + if (that == null) { + return null; + } + int line = that.line; + char prev = 0; + for (int i=0; i=0; i--) { + char c = str.charAt(i); + if ((c == '\r' && prev != '\n') || (c == '\n')) { + line--; + } + prev = c; + } + return new SourcePositionInfo(that.file, line, 0); + } + + public String toString() + { + if (this.file == null) { + return "(unknown)"; + } else { + if (this.line == 0) { + return this.file + ':'; + } else { + return this.file + ':' + this.line + ':'; + } + } + } + + public int compareTo(Object o) { + SourcePositionInfo that = (SourcePositionInfo)o; + int r = this.file.compareTo(that.file); + if (r != 0) return r; + return this.line - that.line; + } + + /** + * Build a SourcePositionInfo from the XML source= notation + */ + public static SourcePositionInfo fromXml(String source) { + if (source != null) { + for (int i = 0; i < source.length(); i++) { + if (source.charAt(i) == ':') { + return new SourcePositionInfo(source.substring(0, i), + Integer.parseInt(source.substring(i+1)), 0); + } + } + } + + return new SourcePositionInfo("(unknown)", 0, 0); + } + + public String file; + public int line; + public int column; +} -- cgit v1.1