package org.yinwang.pysonar.demos;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.yinwang.pysonar.*;
import org.yinwang.pysonar.types.ModuleType;
import java.io.File;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Pattern;
/**
* Collects per-file hyperlinks, as well as styles that require the
* symbol table to resolve properly.
*/
class Linker {
private static final Pattern CONSTANT = Pattern.compile("[A-Z_][A-Z0-9_]*");
// Map of file-path to semantic styles & links for that path.
@NotNull
private Map<String, List<StyleRun>> fileStyles = new HashMap<>();
private File outDir; // where we're generating the output html
private String rootPath;
/**
* Constructor.
* @param root the root of the directory tree being indexed
* @param outdir the html output directory
*/
public Linker(String root, File outdir) {
rootPath = root;
outDir = outdir;
}
/**
* Process all bindings across all files and record per-file semantic styles.
* Should be called once per index.
*/
public void findLinks(@NotNull Indexer indexer) {
Util.msg("Adding xref links");
int ndef = 0;
for (List<Binding> bindings : indexer.getAllBindings().values()) {
for (Binding b : bindings) {
ndef += b.getDefs().size();
}
}
FancyProgress progress = new FancyProgress(ndef, 50);
for (List<Binding> bindings : indexer.getAllBindings().values()) {
for (Binding b : bindings) {
addSemanticStyles(b);
for (Def def : b.getDefs()) {
processDef(def, b);
progress.tick();
}
}
}
// highlight definitions
Util.msg("\nAdding ref links");
progress = new FancyProgress(indexer.getReferences().size(), 50);
for (Entry<Ref,List<Binding>> e : indexer.getReferences().entrySet()) {
processRef(e.getKey(), e.getValue());
progress.tick();
}
// for (List<Diagnostic> ld: indexer.semanticErrors.values()) {
// for (Diagnostic d: ld) {
// processDiagnostic(d);
// }
// }
// for (List<Diagnostic> ld: indexer.parseErrors.values()) {
// for (Diagnostic d: ld) {
// processDiagnostic(d);
// }
// }
}
private void processDef(@Nullable Def def, @NotNull Binding binding) {
if (def == null || def.isURL() || def.getStart() < 0) {
return;
}
StyleRun style = new StyleRun(StyleRun.Type.ANCHOR, def.getStart(), def.getLength());
style.message = binding.getQname() + " : " + binding.getType();
style.url = binding.getQname();
style.id = "" + Math.abs(def.hashCode());
Set<Ref> refs = binding.getRefs();
style.highlight = new ArrayList<>();
for (Ref r : refs) {
style.highlight.add(Integer.toString(Math.abs(r.hashCode())));
}
addFileStyle(def.getFile(), style);
}
void processRef(@NotNull Ref ref, @NotNull List<Binding> bindings) {
StyleRun link = new StyleRun(StyleRun.Type.LINK, ref.start(), ref.length());
link.id = Integer.toString(Math.abs(ref.hashCode()));
List<String> typings = new ArrayList<>();
for (Binding b : bindings) {
typings.add(b.getQname() + " : " + b.getType());
}
link.message = Util.joinWithSep(typings, " | ", "{", "}");
link.highlight = new ArrayList<>();
for (Binding b : bindings) {
for (Def d : b.getDefs()) {
link.highlight.add(Integer.toString(Math.abs(d.hashCode())));
}
}
// Currently jump to the first binding only. Should change to have a
// hover menu or something later.
String path = ref.getFile();
for (Binding b : bindings) {
if (link.url == null) {
link.url = toURL(b, path);
}
if (link.url != null) {
addFileStyle(path, link);
break;
}
}
}
/**
* Returns the styles (links and extra styles) generated for a given file.
* @param path an absolute source path
* @return a possibly-empty list of styles for that path
*/
public List<StyleRun> getStyles(String path) {
return stylesForFile(path);
}
private List<StyleRun> stylesForFile(String path) {
List<StyleRun> styles = fileStyles.get(path);
if (styles == null) {
styles = new ArrayList<StyleRun>();
fileStyles.put(path, styles);
}
return styles;
}
private void addFileStyle(String path, StyleRun style) {
stylesForFile(path).add(style);
}
/**
* Add additional highlighting styles based on information not evident from
* the AST.
*/
private void addSemanticStyles(@NotNull Binding nb) {
Def def = nb.getSingle();
if (def == null || !def.isName()) {
return;
}
boolean isConst = CONSTANT.matcher(def.getName()).matches();
switch (nb.getKind()) {
case SCOPE:
if (isConst) {
addSemanticStyle(def, StyleRun.Type.CONSTANT);
}
break;
case VARIABLE:
addSemanticStyle(def, isConst ? StyleRun.Type.CONSTANT : StyleRun.Type.IDENTIFIER);
break;
case PARAMETER:
addSemanticStyle(def, StyleRun.Type.PARAMETER);
break;
case CLASS:
addSemanticStyle(def, StyleRun.Type.TYPE_NAME);
break;
}
}
private void addSemanticStyle(@NotNull Def def, StyleRun.Type type) {
String path = def.getFile();
if (path != null) {
addFileStyle(path, new StyleRun(type, def.getStart(), def.getLength()));
}
}
private void processDiagnostic(@NotNull Diagnostic d) {
StyleRun style = new StyleRun(StyleRun.Type.WARNING, d.start, d.end - d.start);
style.message = d.msg;
style.url = d.file;
addFileStyle(d.file, style);
}
/**
* Generate a URL for a reference to a binding.
* @param binding the referenced binding
* @param filename the path containing the reference, or null if there was an error
*/
@Nullable
private String toURL(@NotNull Binding binding, String filename) {
Def def = binding.getSingle();
if (binding.isBuiltin()) {
return def.getURL();
}
String destPath = null;
if (binding.getKind() == Binding.Kind.MODULE) {
ModuleType mtype = binding.getType().asModuleType();
if (mtype != null) {
destPath = mtype.getFile();
}
} else {
destPath = def.getFile();
}
if (destPath == null) {
return null;
}
String anchor = "#" + binding.getQname();
if (binding.getFirstFile().equals(filename)) {
return anchor;
}
if (destPath.startsWith(rootPath)) {
String relpath;
if (filename != null) {
relpath = Util.relativize(filename, destPath);
} else {
relpath = destPath;
}
if (relpath != null) {
return relpath + ".html" + anchor;
} else {
return anchor;
}
} else {
return "file://" + destPath + anchor;
}
}
}