/******************************************************************************* * Copyright (c) 2015, 2016 GK Software AG. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Stephan Herrmann - initial API and implementation *******************************************************************************/ package org.eclipse.jdt.core.util; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IPackageFragmentRoot; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.compiler.CharOperation; import org.eclipse.jdt.core.dom.IMethodBinding; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.internal.compiler.classfmt.ExternalAnnotationProvider; import org.eclipse.jdt.internal.compiler.lookup.SignatureWrapper; import org.eclipse.jdt.internal.core.ClasspathEntry; import org.eclipse.jdt.internal.core.util.KeyToSignature; /** * Utilities for accessing and manipulating text files that externally define annotations for a given Java type. * Files are assumed to be in ".eea format", a textual representation of annotated signatures of members of a given type. * * @since 3.11 * @noinstantiate This class is not intended to be instantiated by clients. */ public final class ExternalAnnotationUtil { /** Representation of a 'nullable' annotation, independent of the concrete annotation name used in Java sources. */ public static final char NULLABLE = ExternalAnnotationProvider.NULLABLE; /** Representation of a 'nonnull' annotation, independent of the concrete annotation name used in Java sources. */ public static final char NONNULL = ExternalAnnotationProvider.NONNULL; /** * Represents absence of a null annotation. Useful for removing an existing null annotation. * This character is used only internally, it is not part of the Eclipse External Annotation file format. */ public static final char NO_ANNOTATION = ExternalAnnotationProvider.NO_ANNOTATION; /** Strategy for merging a new signature with an existing (possibly annotated) signature. */ public static enum MergeStrategy { /** Unconditionally replace the signature. */ REPLACE_SIGNATURE, /** Override existing annotations, keeping old annotations in locations that are not annotated in the new signature. */ OVERWRITE_ANNOTATIONS, /** Only add new annotations, never remove or overwrite existing annotations. */ ADD_ANNOTATIONS } private static final int POSITION_RETURN_TYPE = -1; private static final int POSITION_FULL_SIGNATURE = -2; /** * Answer the give method's signature in class file format. * @param methodBinding binding representing a method * @return a signature in class file format */ public static String extractGenericSignature(IMethodBinding methodBinding) { // Note that IMethodBinding.binding is not accessible, hence we need to recover the signature from the key: KeyToSignature parser = new KeyToSignature(methodBinding.getKey(), KeyToSignature.SIGNATURE, true); parser.parse(); return parser.toString(); } /** * Answer the given types's signature in class file format. * @param type binding representing a type * @return a signature in class file format */ public static String extractGenericTypeSignature(ITypeBinding type) { KeyToSignature parser = new KeyToSignature(type.getKey(), KeyToSignature.SIGNATURE, true); parser.parse(); return parser.toString(); } /** * Insert an encoded annotation into the given methodSignature affecting its return type. * <p> * This method is suitable for declaration annotations. * </p> * @param methodSignature a method signature in class file format * @param annotation one of {@link #NULLABLE} and {@link #NONNULL}. * @param mergeStrategy when passing {@link MergeStrategy#ADD_ANNOTATIONS} this method will * refuse to overwrite any existing annotation in the specified location * @return the modified method signature, or the original signature if modification would * conflict with the given merge strategy. * @throws IllegalArgumentException if the method signature is malformed or its return type is not a reference type. */ public static String insertReturnAnnotation(String methodSignature, char annotation, MergeStrategy mergeStrategy) { int close = methodSignature.indexOf(')'); if (close == -1 || close > methodSignature.length()-4) throw new IllegalArgumentException("Malformed method signature"); //$NON-NLS-1$ switch (methodSignature.charAt(close+1)) { case 'L': case 'T': case '[': return insertAt(methodSignature, close+2, annotation, mergeStrategy); } throw new IllegalArgumentException("Return type is not a reference type"); //$NON-NLS-1$ } /** * Insert an encoded annotation into the given methodSignature affecting one of its parameters. * <p> * This method is suitable for declaration annotations. * </p> * @param methodSignature a method signature in class file format * @param paramIdx 0-based index of the parameter to which the annotation should be attached * @param annotation one of {@link #NULLABLE} and {@link #NONNULL}. * @param mergeStrategy when passing {@link MergeStrategy#ADD_ANNOTATIONS} this method will * refuse to overwrite any existing annotation in the specified location * @return the modified method signature, or the original signature if modification would * conflict with the given merge strategy. * @throws IllegalArgumentException if the method signature is malformed or its specified parameter type is not a reference type. */ public static String insertParameterAnnotation(String methodSignature, int paramIdx, char annotation, MergeStrategy mergeStrategy) { SignatureWrapper wrapper = new SignatureWrapper(methodSignature.toCharArray()); wrapper.start = 1; for (int i = 0; i < paramIdx; i++) wrapper.start = wrapper.computeEnd() + 1; int start = wrapper.start; switch (methodSignature.charAt(start)) { case 'L': case 'T': case '[': return insertAt(methodSignature, start+1, annotation, mergeStrategy); } throw new IllegalArgumentException("Paramter type is not a reference type"); //$NON-NLS-1$ } /** * Answer the external annotation file corresponding to the given type as seen from the given project. * Note that manipulation of external annotations is only supported for annotation files in the workspace, * and only in directory layout, not from zip files. * @param project current project that references the given type from a jar file. * @param type the type for which external annotations are sought * @param monitor progress monitor to be passed through into file operations * @return a file assumed (but not checked) to be in .eea format. The file may not "exist". * Can be null if the given type is not contained in a jar file for which an external annotation path * has been defined in the context of the given project. * @throws CoreException Signals a problem in accessing any of the relevant elements: the project, the type, * the containing jar file and finally the sought annotation file. */ public static IFile getAnnotationFile(IJavaProject project, ITypeBinding type, IProgressMonitor monitor) throws CoreException { IType targetType = project.findType(type.getErasure().getQualifiedName()); if (!targetType.exists()) return null; String binaryTypeName = targetType.getFullyQualifiedName('$').replace('.', '/'); IPackageFragmentRoot packageRoot = (IPackageFragmentRoot) targetType.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT); IClasspathEntry entry = packageRoot.getResolvedClasspathEntry(); IPath annotationPath = ClasspathEntry.getExternalAnnotationPath(entry, project.getProject(), false); if (annotationPath == null) return null; IWorkspaceRoot workspaceRoot = project.getProject().getWorkspace().getRoot(); if (annotationPath.segmentCount() > 1) { IFile annotationZip = workspaceRoot.getFile(annotationPath); if (annotationZip.exists()) return null; } annotationPath = annotationPath.append(binaryTypeName).addFileExtension(ExternalAnnotationProvider.ANNOTATION_FILE_EXTENSION); return workspaceRoot.getFile(annotationPath); } /** * Update the given external annotation file with details regarding annotations of one specific method or field. * If the specified member already has external annotations, old and new annotations will be merged, * with priorities controlled by the parameter 'mergeStrategy'. * <p> * This method is suitable for declaration annotations and type use annotations. * </p> * @param typeName binary name (slash separated) of the type being annotated * @param file a file assumed to be in .eea format, will be created if it doesn't exist. * @param selector selector of the method or field * @param originalSignature unannotated signature of the member, used for identification * @param annotatedSignature new signatures whose annotations should be superimposed on the member * @param mergeStrategy controls how old and new signatures should be merged * @param monitor progress monitor to be passed through into file operations, or null if no reporting is desired * @throws CoreException if access to the file fails * @throws IOException if reading file content fails */ public static void annotateMember(String typeName, IFile file, String selector, String originalSignature, String annotatedSignature, MergeStrategy mergeStrategy, IProgressMonitor monitor) throws CoreException, IOException { annotateMember(typeName, file, selector, originalSignature, annotatedSignature, POSITION_FULL_SIGNATURE, mergeStrategy, monitor); } /** * Update the given external annotation file with details regarding annotations of the return type of a given method. * If the specified method already has external annotations, old and new annotations will be merged, * with priorities controlled by the parameter 'mergeStrategy'. * <p> * This method is suitable for declaration annotations and type use annotations. * </p> * @param typeName binary name (slash separated) of the type being annotated * @param file a file assumed to be in .eea format, will be created if it doesn't exist. * @param selector selector of the method * @param originalSignature unannotated signature of the member, used for identification * @param annotatedReturnType signature of the new return type whose annotations should be superimposed on the method * @param mergeStrategy controls how old and new signatures should be merged * @param monitor progress monitor to be passed through into file operations, or null if no reporting is desired * @throws CoreException if access to the file fails * @throws IOException if reading file content fails * @throws IllegalArgumentException if the annotatedReturnType does not structurally match to originalSignature */ public static void annotateMethodReturnType(String typeName, IFile file, String selector, String originalSignature, String annotatedReturnType, MergeStrategy mergeStrategy, IProgressMonitor monitor) throws CoreException, IOException, IllegalArgumentException { annotateMember(typeName, file, selector, originalSignature, annotatedReturnType, POSITION_RETURN_TYPE, mergeStrategy, monitor); } /** * Update the given external annotation file with details regarding annotations of a parameter type of a given method. * If the specified method already has external annotations, old and new annotations will be merged, * with priorities controlled by the parameter 'mergeStrategy'. * <p> * This method is suitable for declaration annotations and type use annotations. * </p> * @param typeName binary name (slash separated) of the type being annotated * @param file a file assumed to be in .eea format, will be created if it doesn't exist. * @param selector selector of the method * @param originalSignature unannotated signature of the member, used for identification * @param annotatedParameterType signature of the new parameter type whose annotations should be superimposed on the method * @param paramIdx 0-based index of the parameter to which the annotation should be attached * @param mergeStrategy controls how old and new signatures should be merged * @param monitor progress monitor to be passed through into file operations, or null if no reporting is desired * @throws CoreException if access to the file fails * @throws IOException if reading file content fails * @throws IllegalArgumentException if the annotatedParameterType does not structurally match to originalSignature */ public static void annotateMethodParameterType(String typeName, IFile file, String selector, String originalSignature, String annotatedParameterType, int paramIdx, MergeStrategy mergeStrategy, IProgressMonitor monitor) throws CoreException, IOException, IllegalArgumentException { annotateMember(typeName, file, selector, originalSignature, annotatedParameterType, paramIdx, mergeStrategy, monitor); } private static void annotateMember(String typeName, IFile file, String selector, String originalSignature, String annotatedSignature, int updatePosition, MergeStrategy mergeStrategy, IProgressMonitor monitor) throws CoreException, IOException, IllegalArgumentException { if (!file.exists()) { // assemble full annotatedSignature (don't bother merging since no previous signature exists): annotatedSignature = updateSignature(originalSignature, annotatedSignature, updatePosition, MergeStrategy.REPLACE_SIGNATURE); StringBuffer newContent= new StringBuffer(); // header: newContent.append(ExternalAnnotationProvider.CLASS_PREFIX); newContent.append(typeName).append('\n'); // new entry: newContent.append(selector).append('\n'); newContent.append(' ').append(originalSignature).append('\n'); newContent.append(' ').append(annotatedSignature).append('\n'); createNewFile(file, newContent.toString(), monitor); } else { BufferedReader reader = new BufferedReader(new InputStreamReader(file.getContents())); StringBuffer newContent = new StringBuffer(); try { newContent.append(reader.readLine()).append('\n'); // skip class name String line; while ((line = reader.readLine()) != null) { if (line.isEmpty()) { newContent.append('\n'); continue; } if (!Character.isJavaIdentifierStart(line.charAt(0)) && line.charAt(0) != '<') { newContent.append(line).append('\n'); continue; } // compare selectors: int relation = line.compareTo(selector); if (relation > 0) { // past the insertion point break; } if (relation < 0) { newContent.append(line).append('\n'); continue; } if (relation == 0) { StringBuffer pending = new StringBuffer(line).append('\n'); pending.append(line = reader.readLine()); if (line == null) { break; // found only the selector at EOF, append right here, ignoring 'pending' } // compare original signatures: relation = line.trim().compareTo(originalSignature); if (relation > 0) { // past the insertion point // add new entry (below) line = pending.toString(); // push back break; } newContent.append(pending).append('\n'); if (relation < 0) continue; if (relation == 0) { // update existing entry: String annotationLine = reader.readLine(); String nextLine = null; if (annotationLine == null || annotationLine.isEmpty() || !annotationLine.startsWith(" ")) { //$NON-NLS-1$ nextLine = annotationLine; // push back, since not a signature line annotationLine = line; // no annotated line yet, use unannotated line instead } if (annotationLine.startsWith(" ")) { //$NON-NLS-1$ switch (mergeStrategy) { case REPLACE_SIGNATURE: break; // unconditionally use annotatedSignature case OVERWRITE_ANNOTATIONS: case ADD_ANNOTATIONS: annotatedSignature = updateSignature(annotationLine.trim(), annotatedSignature, updatePosition, mergeStrategy); break; default: JavaCore.getJavaCore().getLog().log(new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, "Unexpected value for enum MergeStrategy")); //$NON-NLS-1$ } } writeFile(file, newContent, annotatedSignature, nextLine, reader, monitor); return; } } } // add new entry: newContent.append(selector).append('\n'); newContent.append(' ').append(originalSignature).append('\n'); annotatedSignature = updateSignature(originalSignature, annotatedSignature, updatePosition, mergeStrategy); writeFile(file, newContent, annotatedSignature, line, reader, monitor); } finally { reader.close(); } } } private static String updateSignature(String originalSignature, String annotatedSignature, int updatePosition, MergeStrategy mergeStrategy) { StringBuffer buf = new StringBuffer(); String signatureToReplace; String postfix = null; switch (updatePosition) { case POSITION_FULL_SIGNATURE: signatureToReplace = originalSignature; break; case POSITION_RETURN_TYPE: assert originalSignature.charAt(0) == '(' || originalSignature.charAt(0) == '<': "signature must start with '(' or '<'"; //$NON-NLS-1$ int close = originalSignature.indexOf(')'); buf.append(originalSignature, 0, close+1); signatureToReplace = originalSignature.substring(close+1); break; default: // parameter SignatureWrapper wrapper = new SignatureWrapper(originalSignature.toCharArray(), true, true); // may already contain annotations wrapper.start = CharOperation.indexOf('(', wrapper.signature) + 1; // possibly skipping type parameters for (int i = 0; i < updatePosition; i++) wrapper.start = wrapper.skipAngleContents(wrapper.computeEnd()) + 1; int start = wrapper.start; int end = wrapper.skipAngleContents(wrapper.computeEnd()); buf.append(originalSignature, 0, start); signatureToReplace = originalSignature.substring(start, end+1); postfix = originalSignature.substring(end+1, originalSignature.length()); } updateType(buf, signatureToReplace.toCharArray(), annotatedSignature.toCharArray(), mergeStrategy); if (postfix != null) buf.append(postfix); return buf.toString(); } /** * Insert that given annotation at the given position into the given signature. * @param mergeStrategy if set to {@link MergeStrategy#ADD_ANNOTATIONS}, refuse to * overwrite any existing annotation in the specified location. */ private static String insertAt(String signature, int position, char annotation, MergeStrategy mergeStrategy) { StringBuffer result = new StringBuffer(); result.append(signature, 0, position); result.append(annotation); char next = signature.charAt(position); switch (next) { case NULLABLE: case NONNULL: if (mergeStrategy == MergeStrategy.ADD_ANNOTATIONS) return signature; // refuse any change position++; // skip old annotation } result.append(signature, position, signature.length()); return result.toString(); } /** * Update 'oldType' with annotations from 'newType' guided by 'mergeStrategy'. * The result is written into 'buf' as we go. */ private static boolean updateType(StringBuffer buf, char[] oldType, char[] newType, MergeStrategy mergeStrategy) { if (mergeStrategy == MergeStrategy.REPLACE_SIGNATURE) { buf.append(newType); return false; } try { SignatureWrapper oWrap = new SignatureWrapper(oldType, true, true); // may already contain annotations SignatureWrapper nWrap = new SignatureWrapper(newType, true, true); // may already contain annotations if (match(buf, oWrap, nWrap, 'L', false) || match(buf, oWrap, nWrap, 'T', false)) { mergeAnnotation(buf, oWrap, nWrap, mergeStrategy); buf.append(oWrap.nextName()); nWrap.nextName(); // skip if (match(buf, oWrap, nWrap, '<', false)) { do { int oStart = oWrap.start; int nStart = nWrap.start; oWrap.computeEnd(); nWrap.computeEnd(); if (updateType(buf, oWrap.getFrom(oStart), nWrap.getFrom(nStart), mergeStrategy)) mergeAnnotation(buf, oWrap, nWrap, mergeStrategy); } while (!match(buf, oWrap, nWrap, '>', false)); } match(buf, oWrap, nWrap, ';', true); } else if (match(buf, oWrap, nWrap, '[', false)) { mergeAnnotation(buf, oWrap, nWrap, mergeStrategy); updateType(buf, oWrap.tail(), nWrap.tail(), mergeStrategy); } else if (match(buf, oWrap, nWrap, '*', false) || match(buf, oWrap, nWrap, '+', false) || match(buf, oWrap, nWrap, '-', false)) { return true; // annotation allowed after this (not included in oldType / newType) } else { buf.append(oldType); } } catch (ArrayIndexOutOfBoundsException aioobe) { // from several locations inside match() or mergeAnnotation(). StringBuilder msg = new StringBuilder("Structural mismatch between ").append(oldType).append(" and ").append(newType); //$NON-NLS-1$ //$NON-NLS-2$ throw new IllegalArgumentException(msg.toString(), aioobe); } return false; } /** * Does the current char at both given signatures match the 'expected' char? * If yes, print it into 'buf' and answer true. * If no, if 'force' raise an exception, else quietly answer false without updating 'buf'. */ private static boolean match(StringBuffer buf, SignatureWrapper sig1, SignatureWrapper sig2, char expected, boolean force) { boolean match1 = sig1.signature[sig1.start] == expected; boolean match2 = sig2.signature[sig2.start] == expected; if (match1 != match2) { StringBuilder msg = new StringBuilder("Mismatching type structures ") //$NON-NLS-1$ .append(sig1.signature).append(" vs ").append(sig2.signature); //$NON-NLS-1$ throw new IllegalArgumentException(msg.toString()); } if (match1) { buf.append(expected); sig1.start++; sig2.start++; return true; } else if (force) { throw new IllegalArgumentException("Expected char "+expected+" not found in "+new String(sig1.signature)); //$NON-NLS-1$ //$NON-NLS-2$ } else { return false; } } /** * If a current char of 'oldS' and/or 'newS' represents a null annotation, insert it into 'buf' guided by 'mergeStrategy'. * If the new char is NO_ANNOTATION and strategy is OVERWRITE_ANNOTATIONS, silently skip over any null annotations in 'oldS'. */ private static void mergeAnnotation(StringBuffer buf, SignatureWrapper oldS, SignatureWrapper newS, MergeStrategy mergeStrategy) { // if atEnd use a char that's different from NULLABLE, NONNULL and NO_ANNOTATION: char oldAnn = !oldS.atEnd() ? oldS.signature[oldS.start] : '\0'; char newAnn = !newS.atEnd() ? newS.signature[newS.start] : '\0'; switch (mergeStrategy) { case ADD_ANNOTATIONS: switch (oldAnn) { case NULLABLE: case NONNULL: oldS.start++; buf.append(oldAnn); // old exists, so it remains switch (newAnn) { case NULLABLE: case NONNULL: newS.start++; } // just skip return; } //$FALL-THROUGH$ case OVERWRITE_ANNOTATIONS: switch (newAnn) { case NULLABLE: case NONNULL: newS.start++; buf.append(newAnn); // new exists and is not suppressed by "ADD & old exists" switch (oldAnn) { case NULLABLE: case NONNULL: oldS.start++; } // just skip break; case NO_ANNOTATION: newS.start++; // don't insert switch (oldAnn) { case NULLABLE: case NONNULL: oldS.start++; } // just skip break; default: switch (oldAnn) { case NULLABLE: case NONNULL: oldS.start++; buf.append(oldAnn); // keep } } break; default: throw new IllegalArgumentException("Unexpected merge strategy"); // REPLACE_SIGNATURE does not reach this point, see initial check in updateType() //$NON-NLS-1$ } } /** * Write back the given annotationFile, with the following content: * - head (assumed to include a member and its original signature * - annotatedSignature * - nextLines (optionally, may be null) * - the still unconsumed content of tailReader */ private static void writeFile(IFile annotationFile, StringBuffer head, String annotatedSignature, String nextLines, BufferedReader tailReader, IProgressMonitor monitor) throws CoreException, IOException { head.append(' ').append(annotatedSignature).append('\n'); if (nextLines != null) head.append(nextLines).append('\n'); String line; while ((line = tailReader.readLine()) != null) head.append(line).append('\n'); ByteArrayInputStream newContent = new ByteArrayInputStream(head.toString().getBytes("UTF-8")); //$NON-NLS-1$ annotationFile.setContents(newContent, IResource.KEEP_HISTORY, monitor); } private static void createNewFile(IFile file, String newContent, IProgressMonitor monitor) throws CoreException { ensureExists(file.getParent(), monitor); try { file.create(new ByteArrayInputStream(newContent.getBytes("UTF-8")), false, monitor); //$NON-NLS-1$ } catch (UnsupportedEncodingException e) { throw new CoreException(new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, e.getMessage(), e)); } } private static void ensureExists(IContainer container, IProgressMonitor monitor) throws CoreException { if (container.exists()) return; if (!(container instanceof IFolder)) throw new CoreException(new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, "not a folder: "+container)); //$NON-NLS-1$ IContainer parent= container.getParent(); if (parent instanceof IFolder) { ensureExists(parent, monitor); } ((IFolder) container).create(false, true, monitor); } /** * Retrieve the annotated signature of a specified member as found in the given external annotation file, if any. * @param typeName fully qualified slash-separated name of the type for which the file defines external annotations * @param file a file assumed to be in .eea format, must not be null, but may not exist * @param selector name of the member whose annotation we are looking for * @param originalSignature the unannotated signature by which the member is identified * @return the annotated signature as found in the file, or null. */ public static String getAnnotatedSignature(String typeName, IFile file, String selector, String originalSignature) { if (file.exists()) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getContents()))) { ExternalAnnotationProvider.assertClassHeader(reader.readLine(), typeName); while (true) { String line = reader.readLine(); // selector: if (selector.equals(line)) { // original signature: line = reader.readLine(); if (originalSignature.equals(ExternalAnnotationProvider.extractSignature(line))) { // annotated signature: return ExternalAnnotationProvider.extractSignature(reader.readLine()); } } if (line == null) break; } } catch (IOException | CoreException e) { return null; } } return null; } /** * Apply the specified changes on the given type. * This method can be used as a dry run without modifying an annotation file. * * @param originalSignature the original type signature, may be annotated already * @param annotatedType a type signature with additional annotations (incl. {@link #NO_ANNOTATION}). * @param mergeStrategy controls how old and new signatures should be merged * @return an array of length four: <ul> * <li>prefix up-to the changed type</li> * <li>original type</li> * <li>changed type</li> * <li>postfix after the changed type <em>(here: empty string)</li> * </ul> */ public static String[] annotateType(String originalSignature, String annotatedType, MergeStrategy mergeStrategy) { String[] result = new String[4]; // prefix, orig, replacement, postfix StringBuffer buf; result[0] = ""; //$NON-NLS-1$ buf = new StringBuffer(); result[1] = originalSignature; updateType(buf, originalSignature.toCharArray(), annotatedType.toCharArray(), mergeStrategy); result[2] = buf.toString(); result[3] = ""; //$NON-NLS-1$ return result; } /** * Apply the specified changes on the return type of the given signature. * This method can be used as a dry run without modifying an annotation file. * * @param originalSignature the original full signature, may be annotated already * @param annotatedType a type signature with additional annotations (incl. {@link #NO_ANNOTATION}). * @param mergeStrategy controls how old and new signatures should be merged * @return an array of length four: <ul> * <li>prefix up-to the changed type</li> * <li>original type</li> * <li>changed type</li> * <li>postfix after the changed type <em>(here: empty string)</li> * </ul> */ public static String[] annotateReturnType(String originalSignature, String annotatedType, MergeStrategy mergeStrategy) { String[] result = new String[4]; // prefix, orig, replacement, postfix StringBuffer buf; assert originalSignature.charAt(0) == '(' || originalSignature.charAt(0) == '<': "signature must start with '(' or '<'"; //$NON-NLS-1$ int close = originalSignature.indexOf(')'); result[0] = originalSignature.substring(0, close+1); buf = new StringBuffer(); result[1] = originalSignature.substring(close+1); updateType(buf, result[1].toCharArray(), annotatedType.toCharArray(), mergeStrategy); result[2] = buf.toString(); result[3] = ""; //$NON-NLS-1$ return result; } /** * Apply the specified changes on a parameter within the given signature. * This method can be used as a dry run without modifying an annotation file. * * @param originalSignature the original full signature, may be annotated already * @param annotatedType a type signature with additional annotations (incl. {@link #NO_ANNOTATION}). * @param paramIdx the index of a parameter to annotated * @param mergeStrategy controls how old and new signatures should be merged * @return an array of length four: <ul> * <li>prefix up-to the changed type</li> * <li>original type</li> * <li>changed type</li> * <li>postfix after the changed type</li> * </ul> */ public static String[] annotateParameterType(String originalSignature, String annotatedType, int paramIdx, MergeStrategy mergeStrategy) { String[] result = new String[4]; // prefix, orig, replacement, postfix StringBuffer buf; SignatureWrapper wrapper = new SignatureWrapper(originalSignature.toCharArray(), true, true); // may already contain annotations wrapper.start = CharOperation.indexOf('(', wrapper.signature) + 1; // possibly skip type parameters for (int i = 0; i < paramIdx; i++) wrapper.start = wrapper.skipAngleContents(wrapper.computeEnd()) + 1; int start = wrapper.start; int end = wrapper.skipAngleContents(wrapper.computeEnd()); result[0] = originalSignature.substring(0, start); buf = new StringBuffer(); result[1] = originalSignature.substring(start, end+1); updateType(buf, result[1].toCharArray(), annotatedType.toCharArray(), mergeStrategy); result[2] = buf.toString(); result[3] = originalSignature.substring(end+1, originalSignature.length()); return result; } }