Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating synthetic classes for unsolved FieldAccessExpr #61

Merged
merged 5 commits into from
Dec 3, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ public Visitable visit(FieldAccessExpr expr, Void p) {
fullNameOfClass = expr.resolve().asField().declaringType().getQualifiedName();
usedMembers.add(fullNameOfClass + "#" + expr.getName().asString());
usedClass.add(fullNameOfClass);
usedClass.add(expr.resolve().getType().describe());
} catch (UnsolvedSymbolException e) {
// if the a field is accessed in the form of a fully-qualified path, such as
// org.example.A.b, then other components in the path apart from the class name and field
Expand Down
20 changes: 16 additions & 4 deletions src/main/java/org/checkerframework/specimin/UnsolvedClass.java
Original file line number Diff line number Diff line change
Expand Up @@ -164,17 +164,29 @@ public void updateFieldByType(String currentType, String correctType) {
Iterator<String> iterator = classFields.iterator();
Set<String> newFields = new HashSet<>();
while (iterator.hasNext()) {
List<String> elements = Splitter.on(' ').splitToList(iterator.next());
String fieldDeclared = iterator.next();
boolean isStatic = false;
if (fieldDeclared.startsWith("static")) {
fieldDeclared = fieldDeclared.replace("static ", "");
isStatic = true;
}
List<String> elements = Splitter.on(' ').splitToList(fieldDeclared);
// fieldExpression is guaranteed to have the form "TYPE FIELD_NAME". Since this field
// expression is from a synthetic class, there is no annotation involved, so TYPE has no
// space.
String fieldType = elements.get(0);
String fieldName = elements.get(1);
if (fieldType.equals(currentType)) {
iterator.remove();
newFields.add(
UnsolvedSymbolVisitor.setInitialValueForVariableDeclaration(
correctType, correctType + " " + fieldName));
if (!isStatic) {
newFields.add(
UnsolvedSymbolVisitor.setInitialValueForVariableDeclaration(
correctType, correctType + " " + fieldName));
} else {
newFields.add(
UnsolvedSymbolVisitor.setInitialValueForVariableDeclaration(
correctType, "static " + correctType + " " + fieldName));
LoiNguyenCS marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

Expand Down
145 changes: 142 additions & 3 deletions src/main/java/org/checkerframework/specimin/UnsolvedSymbolVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ public class UnsolvedSymbolVisitor extends ModifierVisitor<Void> {
/** This instance maps the name of a synthetic method with its synthetic class */
private final Map<String, UnsolvedClass> syntheticMethodAndClass = new HashMap<>();

/**
* This instance maps the name of a synthetic type with the class where there is a field declared
* with that type
*/
private final Map<String, UnsolvedClass> syntheticTypeAndClass = new HashMap<>();

/**
* This is to check if the current synthetic files are enough to prevent UnsolvedSymbolException
* or we still need more.
Expand Down Expand Up @@ -561,6 +567,30 @@ public Visitable visit(MethodDeclaration node, Void arg) {
public Visitable visit(FieldAccessExpr node, Void p) {
if (isASuperCall(node) && !canBeSolved(node)) {
updateSyntheticClassForSuperCall(node);
} else if (canBeSolved(node)) {
return super.visit(node, p);
} else if (isAQualifiedFieldSignature(node.toString())) {
updateClassSetWithQualifiedFieldSignature(node.toString(), true);
} else if (unsolvedFieldCalledByASimpleClassName(node)) {
String simpleClassName = node.getScope().toString();
String fullyQualifiedCall =
classAndPackageMap.getOrDefault(simpleClassName, currentPackage) + "." + node;
updateClassSetWithQualifiedFieldSignature(fullyQualifiedCall, true);
}
// if the symbol that called this field is solvable yet this field is unsolved, then the type of
// the calling symbol is a synthetic class that needs to have a synthetic field updated
else if (canBeSolved(node.getScope())) {
updateSyntheticClassWithNonStaticFields(node);
}

try {
node.resolve();
} catch (UnsolvedSymbolException e) {
// for a qualified name field access such as org.sample.MyClass.field, org.sample will also be
// considered FieldAccessExpr.
if (isAClassPath(node.getScope().toString())) {
this.gotException = true;
}
}
return super.visit(node, p);
}
Expand Down Expand Up @@ -1001,6 +1031,19 @@ public HashSet<String> getParameterFromAMethodDeclaration(MethodDeclaration decl
return setOfParameters;
}

/**
* Given a non-static and unsolved field access expression, this method will update the
* corresponding synthetic class.
*
* @param field a non-static field access expression
*/
public void updateSyntheticClassWithNonStaticFields(FieldAccessExpr field) {
Expression caller = field.getScope();
String fullyQualifiedClassName = caller.calculateResolvedType().describe();
String fieldQualifedSignature = fullyQualifiedClassName + "." + field.getNameAsString();
updateClassSetWithQualifiedFieldSignature(fieldQualifedSignature, false);
}

/**
* For a super call, this method will update the corresponding synthetic class
*
Expand All @@ -1018,13 +1061,13 @@ public void updateSyntheticClassForSuperCall(Expression expr) {
methodAndReturnType.getOrDefault(expr.asMethodCallExpr().getNameAsString(), ""));
} else if (expr instanceof FieldAccessExpr) {
String nameAsString = expr.asFieldAccessExpr().getNameAsString();
updateUnsolvedClassWithFields(
updateUnsolvedSuperClassWithFields(
nameAsString,
getParentClass(className),
classAndPackageMap.getOrDefault(getParentClass(className), this.currentPackage));
} else if (expr instanceof NameExpr) {
String nameAsString = expr.asNameExpr().getNameAsString();
updateUnsolvedClassWithFields(
updateUnsolvedSuperClassWithFields(
nameAsString,
getParentClass(className),
classAndPackageMap.getOrDefault(getParentClass(className), this.currentPackage));
Expand All @@ -1044,7 +1087,7 @@ public void updateSyntheticClassForSuperCall(Expression expr) {
* @param className the name of the synthetic class
* @param packageName the package of the synthetic class
*/
public void updateUnsolvedClassWithFields(
public void updateUnsolvedSuperClassWithFields(
String var, @ClassGetSimpleName String className, String packageName) {
UnsolvedClass relatedClass = new UnsolvedClass(className, packageName);
if (variablesAndDeclaration.containsKey(var)) {
Expand Down Expand Up @@ -1465,6 +1508,24 @@ public boolean unsolvedAndCalledByASimpleClassName(MethodCallExpr method) {
}
}

/**
* Check whether a field, invoked by a simple class name, is unsolved
*
* @param field the field to be checked
* @return true if the field is unsolved and invoked by a simple class name
*/
public boolean unsolvedFieldCalledByASimpleClassName(FieldAccessExpr field) {
try {
field.resolve();
return false;
} catch (UnsolvedSymbolException e) {
// this check is not very comprehensive, since a class can be in lowercase, and a method or
// field can be in uppercase. But since this is without the jar paths, this is the best we can
// do.
return Character.isUpperCase(field.getScope().toString().charAt(0));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't fully understand why being uppercase tells us whether the field is unsolved. Is there some other invariant here that I'm missing, such as that unsolved fields always are static and need to be accessed via a class name?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Professor, I will rename it. We know the field is unsolved because in the visit method for FieldAccessExpr, we check to see if the field is solved first before coming to this check.

Copy link
Collaborator Author

@LoiNguyenCS LoiNguyenCS Dec 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please neglect my earlier response. Before that line, we have a try-catch block to see if the field is solvable. The uppercase check is for the caller of this field. Since we don't have jar paths, we simply assume that if a symbol is capital, then that capital is a class name.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not fully happy with this, but I'll let it go for now as tech debt given the need to move quickly

}
}

/**
* Returns the fully-qualified class name version of a method call invoked by a simple class name.
*
Expand Down Expand Up @@ -1506,6 +1567,20 @@ public boolean isAnUnsolvedStaticMethodCalledByAQualifiedClassName(MethodCallExp
}
}

/**
* This method checks if a field access expression is a qualified field signature. For example,
* for this field access expression: org.package.Class.firstField.secondField, this method will
* return true for "org.package.Class.firstField", but not for
* "org.package.Class.firstField.secondField".
*
* @param field the field access expression to be checked
* @return true if field is a qualified field signature
*/
public boolean isAQualifiedFieldSignature(String field) {
String caller = field.substring(0, field.lastIndexOf("."));
return isAClassPath(caller);
}

/**
* Creates a synthetic class corresponding to a static method called by a qualified class name.
* Ensure to check with {@link #isAnUnsolvedStaticMethodCalledByAQualifiedClassName} before
Expand Down Expand Up @@ -1565,6 +1640,62 @@ public void updateClassSetWithQualifiedStaticMethodCall(
this.updateMissingClass(classThatContainMethod);
}

/**
* Creates a synthetic class corresponding to a static field called by a qualified class name.
* Ensure to check with {@link #isAQualifiedFieldSignature(String)} before calling this method.
*
* @param fieldExpr the field access expression to be used as input. This field access expression
* must be in the form of a qualified class name
* @param isStatic check whether the field is static
*/
public void updateClassSetWithQualifiedFieldSignature(String fieldExpr, boolean isStatic) {
// As this code involves complex string operations, we'll use a field access expression as an
// example,
// following its progression through the code.
// Suppose this is our field access expression: com.example.MyClass.myField
List<String> fieldParts = Splitter.onPattern("[.]").splitToList(fieldExpr);
int numOfFieldParts = fieldParts.size();
if (numOfFieldParts <= 2) {
throw new RuntimeException(
"Need to check this field access expression with"
+ " isAnUnsolvedStaticFieldCalledByAQualifiedClassName before using this method");
}
String fieldTypeClassName = toCapital(fieldParts.get(0));
LoiNguyenCS marked this conversation as resolved.
Show resolved Hide resolved
String packageName = fieldParts.get(0);
// According to the above example, fieldName will be myField
String fieldName = fieldParts.get(numOfFieldParts - 1);
@SuppressWarnings(
"signature") // this className is from the second-to-last part of a fully-qualified field
// signature, which is the simple name of a class. In this case, it is MyClass.
@ClassGetSimpleName String className = fieldParts.get(numOfFieldParts - 2);
// After this loop: fieldTypeClassName will be ComExample, and packageName will be com.example
for (int i = 1; i < numOfFieldParts - 2; i++) {
fieldTypeClassName = fieldTypeClassName + toCapital(fieldParts.get(i));
packageName = packageName + "." + fieldParts.get(i);
}
// At this point, fieldTypeClassName will be ComExampleMyClassMyFieldType
fieldTypeClassName = fieldTypeClassName + toCapital(className) + toCapital(fieldName) + "Type";
// since fieldTypeClassName is just a single long string without any dot in the middle, it will
// be a simple name.
@SuppressWarnings("signature")
@ClassGetSimpleName String thisFieldType = fieldTypeClassName;
UnsolvedClass typeClass = new UnsolvedClass(thisFieldType, packageName);
UnsolvedClass classThatContainField = new UnsolvedClass(className, packageName);
// at this point, fieldDeclaration will become "ComExampleMyClassMyFieldType myField"
String fieldDeclaration = fieldTypeClassName + " " + fieldName;
if (isStatic) {
// fieldDeclaration will become "static ComExampleMyClassMyFieldType myField = null;"
fieldDeclaration = "static " + fieldDeclaration;
}
fieldDeclaration = setInitialValueForVariableDeclaration(fieldTypeClassName, fieldDeclaration);
classThatContainField.addFields(fieldDeclaration);
classAndPackageMap.put(thisFieldType, packageName);
classAndPackageMap.put(className, packageName);
syntheticTypeAndClass.put(thisFieldType, classThatContainField);
this.updateMissingClass(typeClass);
this.updateMissingClass(classThatContainField);
}

/**
* Based on the Map returned by JavaTypeCorrect, this method updates the types of methods in
* synthetic classes.
Expand All @@ -1573,6 +1704,14 @@ public void updateClassSetWithQualifiedStaticMethodCall(
*/
public void updateTypes(Map<String, String> typeToCorrect) {
for (String incorrectType : typeToCorrect.keySet()) {
// update incorrecType if it is the type of a field in a synthetic class
if (syntheticTypeAndClass.containsKey(incorrectType)) {
UnsolvedClass relatedClass = syntheticTypeAndClass.get(incorrectType);
relatedClass.updateFieldByType(incorrectType, typeToCorrect.get(incorrectType));
this.deleteOldSyntheticClass(relatedClass);
this.createMissingClass(relatedClass);
return;
}
// convert MethodNameReturnType to methodName
String involvedMethod =
incorrectType.substring(0, 1).toLowerCase()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.checkerframework.specimin;

import java.io.IOException;
import org.junit.Test;

/**
* This test checks that if Specimin will work if there is an unsolved, static field in a qualified
* name form used by a target method.
*/
public class UnsolvedStaticQualifiedField {
@Test
public void runTest() throws IOException {
SpeciminTestExecutor.runTestWithoutJarPaths(
"unsolvedstaticqualifiedfield",
new String[] {"com/example/Foo.java"},
new String[] {"com.example.Foo#bar()"});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.checkerframework.specimin;

import java.io.IOException;
import org.junit.Test;

/**
* This test checks that if Specimin will work if there is an unsolved, static field in a simple
* name form used by target methods
*/
public class UnsolvedStaticSimpleField {
@Test
public void runTest() throws IOException {
SpeciminTestExecutor.runTestWithoutJarPaths(
"unsolvedstaticsimplefield",
new String[] {"com/example/Foo.java"},
new String[] {"com.example.Foo#bar()"});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.checkerframework.specimin;

import java.io.IOException;
import org.junit.Test;

/**
* This test checks that if Specimin will work if there is an unsolved, static field in a simple
* name form with a primitive type.
*/
public class UnsolvedStaticSimplePrimitiveField {
@Test
public void runTest() throws IOException {
SpeciminTestExecutor.runTestWithoutJarPaths(
"unsolvedstaticsimpleprimitivefield",
new String[] {"com/example/Foo.java"},
new String[] {"com.example.Foo#bar()"});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example;

import org.sampling.Baz;

class Foo {

void test() {
Baz testing = new Baz();
testing.cal.doAddition();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.sampling;

public class Baz {

public OrgSamplingBazCalType cal;

public Baz() {
throw new Error();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.sampling;

public class DoAdditionReturnType {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.sampling;
LoiNguyenCS marked this conversation as resolved.
Show resolved Hide resolved

public class OrgSamplingBazCalType {

public DoAdditionReturnType doAddition() {
throw new Error();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example;
import org.sampling.Baz;
class Foo {
void test() {
Baz testing = new Baz();
testing.cal.doAddition();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example;

class Foo {

void bar() {
org.sampling.Baz.myField.doAddition();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.sampling;

public class Baz {

public static OrgSamplingBazMyFieldType myField;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.sampling;

public class DoAdditionReturnType {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.sampling;

public class OrgSamplingBazMyFieldType {

public DoAdditionReturnType doAddition() {
throw new Error();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example;

class Foo {
void bar() {
org.sampling.Baz.myField.doAddition();
}
}

Loading