Skip to content
This repository has been archived by the owner on May 4, 2023. It is now read-only.

Commit

Permalink
Merge pull request #224 from codiga/rosie-javascript-support
Browse files Browse the repository at this point in the history
#221: Added JavaScript and TypeScript language support.
  • Loading branch information
dastrong-codiga authored Dec 15, 2022
2 parents 8cc9b13 + 55ba544 commit bdb7ac2
Show file tree
Hide file tree
Showing 11 changed files with 233 additions and 94 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public final class RosieRulesCache implements Disposable {

private final Project project;
/**
* Mapping the rules to their target languages, because this way
* Mapping the rules to their target Rosie languages, because this way
* <ul>
* <li>retrieving the rules from this cache is much easier,</li>
* <li>filtering the rules by language each time a request has to be sent to
Expand Down Expand Up @@ -165,10 +165,20 @@ private void reAnalyzeConfigFile() {
* provided language
*/
public List<RosieRule> getRosieRulesForLanguage(LanguageEnumeration language) {
var cachedRules = cache.get(language);
var cachedRules = cache.get(getCachedLanguageTypeOf(language));
return cachedRules != null ? cachedRules.getRosieRules() : List.of();
}

/**
* Since, besides JavaScript files, rules for TypeScript files are also handled under the same JavaScript Rosie language
* type, we have to return JavaScript rules for TypeScript files as well.
*
* @param fileLanguage the file language to map
*/
private LanguageEnumeration getCachedLanguageTypeOf(LanguageEnumeration fileLanguage) {
return fileLanguage == LanguageEnumeration.TYPESCRIPT ? LanguageEnumeration.JAVASCRIPT : fileLanguage;
}

/**
* Returns the cached rules for the provided language and rule id.
* <p>
Expand All @@ -179,7 +189,7 @@ public List<RosieRule> getRosieRulesForLanguage(LanguageEnumeration language) {
* cached here.
*/
public RuleWithNames getRuleWithNamesFor(LanguageEnumeration language, String ruleId) {
return cache.get(language).getRules().get(ruleId);
return cache.get(getCachedLanguageTypeOf(language)).getRules().get(ruleId);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.codiga.plugins.jetbrains.model.rosie;

import io.codiga.plugins.jetbrains.rosie.RosieApiImpl;
import io.codiga.plugins.jetbrains.utils.RosieLanguageSupport;
import lombok.AllArgsConstructor;

import java.util.List;
Expand All @@ -14,8 +14,7 @@ public class RosieRequest {
/**
* The Rosie language string.
* <p>
* See {@link io.codiga.plugins.jetbrains.utils.RosieUtils#getRosieLanguage(io.codiga.api.type.LanguageEnumeration)}
* and {@link RosieApiImpl#SUPPORTED_LANGUAGES}.
* @see RosieLanguageSupport
*/
public String language;
public String fileEncoding;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.codiga.plugins.jetbrains.model.rosie;

import io.codiga.api.GetRulesetsForClientQuery;
import io.codiga.api.type.ElementCheckedEnumeration;
import lombok.ToString;

/**
Expand All @@ -16,44 +15,12 @@ public class RosieRule {
public String entityChecked;
public String pattern;

public final static String ENTITY_CHECKED_FUNCTION_CALL = "functioncall";
public final static String ENTITY_CHECKED_IF_CONDITION = "ifcondition";
public final static String ENTITY_CHECKED_IMPORT = "import";
public final static String ENTITY_CHECKED_ASSIGNMENT = "assign";
public final static String ENTITY_CHECKED_FOR_LOOP = "forloop";
public final static String ENTITY_CHECKED_FUNCTION_DEFINITION = "functiondefinition";
public final static String ENTITY_CHECKED_TRY_BLOCK = "tryblock";

private String elementCheckedToRosieEntityChecked(ElementCheckedEnumeration elementChecked) {
if (elementChecked == null) {
return null;
}
switch (elementChecked) {
case FORLOOP:
return ENTITY_CHECKED_FOR_LOOP;
case ASSIGNMENT:
return ENTITY_CHECKED_ASSIGNMENT;
case FUNCTIONDEFINITION:
return ENTITY_CHECKED_FUNCTION_DEFINITION;
case TRYBLOCK:
return ENTITY_CHECKED_TRY_BLOCK;
case IMPORT:
return ENTITY_CHECKED_IMPORT;
case IFCONDITION:
return ENTITY_CHECKED_IF_CONDITION;
case FUNCTIONCALL:
return ENTITY_CHECKED_FUNCTION_CALL;
default:
return null;
}
}

public RosieRule(String rulesetName, GetRulesetsForClientQuery.Rule rule) {
this.id = rulesetName + "/" + rule.name();
this.contentBase64 = rule.content();
this.language = rule.language().rawValue();
this.type = rule.ruleType().rawValue();
this.entityChecked = elementCheckedToRosieEntityChecked(rule.elementChecked());
this.entityChecked = RosieRuleAstTypes.elementCheckedToRosieEntityChecked(rule.elementChecked());
this.pattern = rule.pattern();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.codiga.plugins.jetbrains.model.rosie;

import io.codiga.api.type.ElementCheckedEnumeration;

/**
* Provides values and mapping logic for rule AST types.
*/
final class RosieRuleAstTypes {
private final static String ENTITY_CHECKED_FUNCTION_CALL = "functioncall";
private final static String ENTITY_CHECKED_IF_CONDITION = "ifcondition";
private final static String ENTITY_CHECKED_IMPORT = "import";
private final static String ENTITY_CHECKED_ASSIGNMENT = "assign";
private final static String ENTITY_CHECKED_FOR_LOOP = "forloop";
private final static String ENTITY_CHECKED_FUNCTION_DEFINITION = "functiondefinition";
private final static String ENTITY_CHECKED_TRY_BLOCK = "tryblock";
private final static String ENTITY_CHECKED_TYPE = "type";
private final static String ENTITY_CHECKED_INTERFACE = "interface";
private final static String ENTITY_CHECKED_HTML_ELEMENT = "htmlelement";
private final static String ENTITY_CHECKED_CLASS_DEFINITION = "classdefinition";
private final static String ENTITY_CHECKED_FUNCTION_EXPRESSION = "functionexpression";

/**
* Maps the argument element checked to its Rosie counterpart value.
*/
static String elementCheckedToRosieEntityChecked(ElementCheckedEnumeration elementChecked) {
if (elementChecked == null) {
return null;
}
switch (elementChecked) {
case FORLOOP:
return ENTITY_CHECKED_FOR_LOOP;
case ASSIGNMENT:
return ENTITY_CHECKED_ASSIGNMENT;
case FUNCTIONDEFINITION:
return ENTITY_CHECKED_FUNCTION_DEFINITION;
case TRYBLOCK:
return ENTITY_CHECKED_TRY_BLOCK;
case IMPORT:
return ENTITY_CHECKED_IMPORT;
case IFCONDITION:
return ENTITY_CHECKED_IF_CONDITION;
case FUNCTIONCALL:
return ENTITY_CHECKED_FUNCTION_CALL;
case TYPE:
return ENTITY_CHECKED_TYPE;
case INTERFACE:
return ENTITY_CHECKED_INTERFACE;
case HTMLELEMENT:
return ENTITY_CHECKED_HTML_ELEMENT;
case CLASSDEFINITION:
return ENTITY_CHECKED_CLASS_DEFINITION;
case FUNCTIONEXPRESSION:
return ENTITY_CHECKED_FUNCTION_EXPRESSION;
default:
return null;
}
}

private RosieRuleAstTypes() {
//Utility class
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package io.codiga.plugins.jetbrains.rosie;

import com.intellij.openapi.project.Project;

/**
* Provides default ruleset configurations for the Codiga config file.
*
* @see io.codiga.plugins.jetbrains.starter.RosieStartupActivity#showConfigureDefaultConfigFileNotification(Project)
* @see io.codiga.plugins.jetbrains.starter.RosieStartupActivity#showConfigureDefaultConfigFileNotification(com.intellij.openapi.project.Project)
*/
public final class CodigaRulesetConfigs {

Expand Down
44 changes: 20 additions & 24 deletions src/main/java/io/codiga/plugins/jetbrains/rosie/RosieApiImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
import java.util.List;

import static io.codiga.plugins.jetbrains.Constants.LOGGER_NAME;
import static io.codiga.plugins.jetbrains.utils.RosieUtils.getRosieLanguage;
import static io.codiga.plugins.jetbrains.utils.RosieLanguageSupport.getRosieLanguage;
import static io.codiga.plugins.jetbrains.utils.RosieLanguageSupport.isLanguageSupported;
import static io.codiga.plugins.jetbrains.utils.UserAgentUtils.getUserAgent;
import static java.util.stream.Collectors.toList;

Expand All @@ -40,13 +41,6 @@ public class RosieApiImpl implements RosieApi {
private static final String ROSIE_POST_URL = "https://analysis.codiga.io/analyze";
private static final Gson GSON = new Gson();

/**
* Current supported languages by Rosie.
* <p>
* See also {@link io.codiga.plugins.jetbrains.utils.RosieUtils#getRosieLanguage(LanguageEnumeration)}.
*/
private static final List<LanguageEnumeration> SUPPORTED_LANGUAGES = List.of(LanguageEnumeration.PYTHON);

public RosieApiImpl() {
// no constructor instructions
}
Expand All @@ -58,11 +52,11 @@ public List<RosieAnnotation> getAnnotations(@NotNull PsiFile psiFile, @NotNull P
return List.of();
}

LanguageEnumeration language = LanguageUtils.getLanguageFromFilename(psiFile.getVirtualFile().getCanonicalPath());
LanguageEnumeration fileLanguage = LanguageUtils.getLanguageFromFilename(psiFile.getVirtualFile().getCanonicalPath());

// not supported, we exit right away
if (!SUPPORTED_LANGUAGES.contains(language)) {
LOGGER.info(String.format("language not supported %s", language));
if (!isLanguageSupported(fileLanguage)) {
LOGGER.info(String.format("language not supported %s", fileLanguage));
return List.of();
}

Expand All @@ -71,13 +65,13 @@ public List<RosieAnnotation> getAnnotations(@NotNull PsiFile psiFile, @NotNull P
String codeBase64 = Base64.getEncoder().encodeToString(fileText);

// Prepare the request
var rosieRules = RosieRulesCache.getInstance(project).getRosieRulesForLanguage(language);
var rosieRules = RosieRulesCache.getInstance(project).getRosieRulesForLanguage(fileLanguage);
//If there is no rule for the target language, then Rosie is not called, and no annotation is performed
if (rosieRules.isEmpty()) {
return List.of();
}

RosieRequest request = new RosieRequest(psiFile.getName(), getRosieLanguage(language), "utf8", codeBase64, rosieRules, true);
RosieRequest request = new RosieRequest(psiFile.getName(), getRosieLanguage(fileLanguage), "utf8", codeBase64, rosieRules, true);
String requestString = GSON.toJson(request);
StringEntity postingString = new StringEntity(requestString); //gson.toJson() converts your pojo to json
HttpPost httpPost = new HttpPost(ROSIE_POST_URL);
Expand All @@ -91,21 +85,23 @@ public List<RosieAnnotation> getAnnotations(@NotNull PsiFile psiFile, @NotNull P
LOGGER.debug("Rules sent in request " + requestTimestamp + ": " + rosieRules.stream().map(RosieRule::toString).collect(toList()));
List<RosieAnnotation> annotations = List.of();
if (response.getEntity() != null) {

String result = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
LOGGER.debug("Response received from request " + requestTimestamp + ": " + result);
RosieResponse rosieResponse = GSON.fromJson(result, RosieResponse.class);

annotations = rosieResponse.ruleResponses.stream()
.flatMap(res -> res.violations.stream()
//distinct()' makes sure that if multiple, completely identical, violations are returned
// for the same problem from Rosie, only one instance is shown by RosieAnnotator.
.distinct()
.map(violation -> {
var rule = RosieRulesCache.getInstance(project).getRuleWithNamesFor(language, res.identifier);
return new RosieAnnotation(rule.ruleName, rule.rulesetName, violation);
}))
.collect(toList());
//If there is no error returned, collect the violations
if (rosieResponse.errors == null || rosieResponse.errors.isEmpty()) {
annotations = rosieResponse.ruleResponses.stream()
.flatMap(res -> res.violations.stream()
//distinct()' makes sure that if multiple, completely identical, violations are returned
// for the same problem from Rosie, only one instance is shown by RosieAnnotator.
.distinct()
.map(violation -> {
var rule = RosieRulesCache.getInstance(project).getRuleWithNamesFor(fileLanguage, res.identifier);
return new RosieAnnotation(rule.ruleName, rule.rulesetName, violation);
}))
.collect(toList());
}
}
client.close();
return annotations;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.codiga.plugins.jetbrains.utils;

import io.codiga.api.type.LanguageEnumeration;

import java.util.Set;

/**
* Utility for the Rosie integration.
*/
public final class RosieLanguageSupport {

/**
* Currently supported languages by Rosie.
*/
private static final Set<LanguageEnumeration> SUPPORTED_LANGUAGES = Set.of(
LanguageEnumeration.PYTHON,
LanguageEnumeration.JAVASCRIPT,
LanguageEnumeration.TYPESCRIPT);

/**
* Returns whether the argument language is supported by Rosie.
*
* @param fileLanguage the language to check
*/
public static boolean isLanguageSupported(LanguageEnumeration fileLanguage) {
return SUPPORTED_LANGUAGES.contains(fileLanguage);
}

/**
* Returns the Rosie language string of the argument Codiga language.
*
* @param language the Codiga language to map to Rosie language
*/
public static String getRosieLanguage(LanguageEnumeration language) {
switch (language) {
case PYTHON:
return "python";
case JAVASCRIPT:
return "javascript";
case TYPESCRIPT:
return "typescript";
default:
return "unknown";
}
}

private RosieLanguageSupport() {
//Utility class
}
}
27 changes: 0 additions & 27 deletions src/main/java/io/codiga/plugins/jetbrains/utils/RosieUtils.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@ public void testReturnsRulesForLanguage() {
assertTrue(javaRules.stream().allMatch(rule -> LanguageEnumeration.JAVA.rawValue().equals(rule.language)));
}

public void testReturnsJavaScriptRulesForTypeScript() {
var cache = RosieRulesCache.getInstance(getProject());
var rulesetNames = List.of("javascriptRuleset");
var rulesets = CodigaApi.getInstance().getRulesetsForClient(rulesetNames);

cache.updateCacheFrom(rulesets.get());

var jsRules = cache.getRosieRulesForLanguage(LanguageEnumeration.TYPESCRIPT);
assertTrue(jsRules.stream().allMatch(rule -> LanguageEnumeration.JAVASCRIPT.rawValue().equals(rule.language)));
}

//Helpers

private void validateRuleCountAndRuleIds(List<RosieRule> rules, int count, String... ruleIds) {
Expand Down
Loading

0 comments on commit bdb7ac2

Please sign in to comment.