aboutsummaryrefslogtreecommitdiff
path: root/src/core/lombok/eclipse/TransformEclipseAST.java
blob: 87b163c0bef7859a9fd67fdfe178bafd7accde76 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
/*
 * Copyright (C) 2009-2020 The Project Lombok Authors.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package lombok.eclipse;

import static lombok.eclipse.handlers.EclipseHandlerUtil.*;

import java.lang.reflect.Field;
import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;

import lombok.ConfigurationKeys;
import lombok.core.LombokConfiguration;
import lombok.core.debug.DebugSnapshotStore;
import lombok.core.debug.HistogramTracker;
import lombok.patcher.Symbols;
import lombok.permit.Permit;

import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
import org.eclipse.jdt.internal.compiler.ast.LocalDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeReference;
import org.eclipse.jdt.internal.compiler.parser.Parser;

/**
 * Entry point for the Eclipse Parser patch that lets lombok modify the Abstract Syntax Tree as generated by
 * Eclipse's parser implementations. This class is injected into the appropriate OSGi ClassLoader and can thus
 * use any classes that belong to org.eclipse.jdt.(apt.)core.
 * 
 * Note that, for any Method body, if Bit24 is set, the Eclipse parser has been patched to never attempt to
 * (re)parse it. You should set Bit24 on any MethodDeclaration object you inject into the AST:
 * 
 * {@code methodDeclaration.bits |= ASTNode.Bit24; //0x800000}
 */
public class TransformEclipseAST {
	private final EclipseAST ast;
	//The patcher hacks this field onto CUD. It's public.
	private static final Field astCacheField;
	private static final HandlerLibrary handlers;
	
	public static boolean disableLombok = false;
	private static final HistogramTracker lombokTracker;
	private static Map<CompilationUnitDeclaration, State> transformationStates = Collections.synchronizedMap(new WeakHashMap<CompilationUnitDeclaration, State>());
	
	static {
		String v = System.getProperty("lombok.histogram");
		if (v == null) lombokTracker = null;
		else if (v.toLowerCase().equals("sysout")) lombokTracker = new HistogramTracker("lombok.histogram", System.out);
		else lombokTracker = new HistogramTracker("lombok.histogram");
	}
	
	static {
		Field f = null;
		HandlerLibrary h = null;
		
		if (System.getProperty("lombok.disable") != null) {
			disableLombok = true;
			astCacheField = null;
			handlers = null;
		} else {
			try {
				h = HandlerLibrary.load();
			} catch (Throwable t) {
				try {
					error(null, "Problem initializing lombok", t);
				} catch (Throwable t2) {
					System.err.println("Problem initializing lombok");
					t.printStackTrace();
				}
				disableLombok = true;
			}
			try {
				f = Permit.getField(CompilationUnitDeclaration.class, "$lombokAST");
			} catch (Throwable t) {
				//I guess we're in an ecj environment; we'll just not cache stuff then.
			}
			
			astCacheField = f;
			handlers = h;
		}
	}
	
	public static void transform_swapped(CompilationUnitDeclaration ast, Parser parser) {
		transform(parser, ast);
	}
	
	public static EclipseAST getAST(CompilationUnitDeclaration ast, boolean forceRebuild) {
		EclipseAST existing = null;
		if (astCacheField != null) {
			try {
				existing = (EclipseAST) astCacheField.get(ast);
			} catch (Exception e) {
				// existing remains null
			}
		}
		
		if (existing == null) {
			existing = new EclipseAST(ast);
			if (astCacheField != null) try {
				astCacheField.set(ast, existing);
			} catch (Exception ignore) {
			}
		} else {
			existing.rebuild(forceRebuild);
		}
		
		return existing;
	}
	
	/**
	 * Check if lombok already handled the given AST. This method will return
	 * <code>true</code> once for diet mode and once for full mode.
	 * 
	 * The reason for this is that Eclipse invokes the transform method multiple
	 * times during compilation and it is enough to transform it once and not
	 * repeat the whole thing over and over again.
	 * 
	 * @param ast The AST node belonging to the compilation unit (java speak for a single source file).
	 * @return <code>true</code> if this AST was already handled by lombok.
	 */
	public static boolean alreadyTransformed(CompilationUnitDeclaration ast) {
		State state = transformationStates.get(ast);
		
		if (state == State.FULL) return true;
		if (state == State.DIET) {
			if (!EclipseAST.isComplete(ast)) return true;
			transformationStates.put(ast, State.FULL);
		} else {
			transformationStates.put(ast, State.DIET);
		}
		return false;
	}
	
	/**
	 * This method is called immediately after Eclipse finishes building a CompilationUnitDeclaration, which is
	 * the top-level AST node when Eclipse parses a source file. The signature is 'magic' - you should not
	 * change it!
	 * 
	 * Eclipse's parsers often operate in diet mode, which means many parts of the AST have been left blank.
	 * Be ready to deal with just about anything being null, such as the Statement[] arrays of the Method AST nodes.
	 * 
	 * @param parser The Eclipse parser object that generated the AST. Not actually used; mostly there to satisfy parameter rules for lombok.patcher scripts.
	 * @param ast The AST node belonging to the compilation unit (java speak for a single source file).
	 */
	public static void transform(Parser parser, CompilationUnitDeclaration ast) {
		if (disableLombok) return;
		
		if (Symbols.hasSymbol("lombok.disable")) return;
		if (alreadyTransformed(ast)) return;
		
		// Do NOT abort if (ast.bits & ASTNode.HasAllMethodBodies) != 0 - that doesn't work.
		
		if (Boolean.TRUE.equals(LombokConfiguration.read(ConfigurationKeys.LOMBOK_DISABLE, EclipseAST.getAbsoluteFileLocation(ast)))) return;
		
		try {
			DebugSnapshotStore.INSTANCE.snapshot(ast, "transform entry");
			long histoToken = lombokTracker == null ? 0L : lombokTracker.start();
			EclipseAST existing = getAST(ast, false);
			existing.setSource(parser.scanner.getSource());
			new TransformEclipseAST(existing).go();
			if (lombokTracker != null) lombokTracker.end(histoToken);
			DebugSnapshotStore.INSTANCE.snapshot(ast, "transform exit");
		} catch (Throwable t) {
			DebugSnapshotStore.INSTANCE.snapshot(ast, "transform error: %s", t.getClass().getSimpleName());
			try {
				String message = "Lombok can't parse this source: " + t.toString();
				
				EclipseAST.addProblemToCompilationResult(ast.getFileName(), ast.compilationResult, false, message, 0, 0);
				t.printStackTrace();
			} catch (Throwable t2) {
				try {
					error(ast, "Can't create an error in the problems dialog while adding: " + t.toString(), t2);
				} catch (Throwable t3) {
					//This seems risky to just silently turn off lombok, but if we get this far, something pretty
					//drastic went wrong. For example, the eclipse help system's JSP compiler will trigger a lombok call,
					//but due to class loader shenanigans we'll actually get here due to a cascade of
					//ClassNotFoundErrors. This is the right action for the help system (no lombok needed for that JSP compiler,
					//of course). 'disableLombok' is static, but each context classloader (e.g. each eclipse OSGi plugin) has
					//it's own edition of this class, so this won't turn off lombok everywhere.
					disableLombok = true;
				}
			}
		}
	}
	
	public TransformEclipseAST(EclipseAST ast) {
		this.ast = ast;
	}
	
	/**
	 * First handles all lombok annotations except PrintAST, then calls all non-annotation based handlers.
	 * then handles any PrintASTs.
	 */
	public void go() {
		long nextPriority = Long.MIN_VALUE;
		for (Long d : handlers.getPriorities()) {
			if (nextPriority > d) continue;
			AnnotationVisitor visitor = new AnnotationVisitor(d);
			ast.traverse(visitor);
			// if no visitor interested for this AST, nextPriority would be MAX_VALUE and we bail out immediatetly
			nextPriority = visitor.getNextPriority();
			nextPriority = Math.min(nextPriority, handlers.callASTVisitors(ast, d, ast.isCompleteParse()));
		}
	}
	
	private static class AnnotationVisitor extends EclipseASTAdapter {
		private final long priority;
		// this is the next priority we continue to visit.
		// Long.MAX_VALUE means never. Each visit method will potentially reduce the next priority
		private long nextPriority = Long.MAX_VALUE;
		
		public AnnotationVisitor(long priority) {
			this.priority = priority;
		}
		
		public long getNextPriority() {
			return nextPriority;
		}
		
		@Override public void visitAnnotationOnField(FieldDeclaration field, EclipseNode annotationNode, Annotation annotation) {
			CompilationUnitDeclaration top = (CompilationUnitDeclaration) annotationNode.top().get();
			nextPriority = Math.min(nextPriority, handlers.handleAnnotation(top, annotationNode, annotation, priority));
		}
		
		@Override public void visitAnnotationOnMethodArgument(Argument arg, AbstractMethodDeclaration method, EclipseNode annotationNode, Annotation annotation) {
			CompilationUnitDeclaration top = (CompilationUnitDeclaration) annotationNode.top().get();
			nextPriority = Math.min(nextPriority, handlers.handleAnnotation(top, annotationNode, annotation, priority));
		}
		
		@Override public void visitAnnotationOnLocal(LocalDeclaration local, EclipseNode annotationNode, Annotation annotation) {
			CompilationUnitDeclaration top = (CompilationUnitDeclaration) annotationNode.top().get();
			nextPriority = Math.min(nextPriority, handlers.handleAnnotation(top, annotationNode, annotation, priority));
		}
		
		@Override public void visitAnnotationOnMethod(AbstractMethodDeclaration method, EclipseNode annotationNode, Annotation annotation) {
			CompilationUnitDeclaration top = (CompilationUnitDeclaration) annotationNode.top().get();
			nextPriority = Math.min(nextPriority, handlers.handleAnnotation(top, annotationNode, annotation, priority));
		}
		
		@Override public void visitAnnotationOnType(TypeDeclaration type, EclipseNode annotationNode, Annotation annotation) {
			CompilationUnitDeclaration top = (CompilationUnitDeclaration) annotationNode.top().get();
			nextPriority = Math.min(nextPriority, handlers.handleAnnotation(top, annotationNode, annotation, priority));
		}
		
		@Override public void visitAnnotationOnTypeUse(TypeReference typeUse, EclipseNode annotationNode, Annotation annotation) {
			CompilationUnitDeclaration top = (CompilationUnitDeclaration) annotationNode.top().get();
			nextPriority = Math.min(nextPriority, handlers.handleAnnotation(top, annotationNode, annotation, priority));
		}
	}
	
	private static enum State {
		DIET,
		FULL
	}
}