/* * ddepcheck.d * Version 1.0.2 * Last modified 22nd of January 2005 * * Dependency walker for D source files. * Copyright 2003-2005 Lars Ivar Igesund * * Permission to use, copy, modify, distribute and sell this software * and its documentation for any purpose is hereby granted without fee, * provided that the above copyright notice appear in all copies and * that both that copyright notice and this permission notice appear * in supporting documentation. It is provided "as is" without express * or implied warranty. */ /* * TODO: * - Audit code, some of it is hairy as hell. * - Exchange ctype with utype when it shows up * - Document better, especially code * - Long term; With multiple input files, cache import results. */ import std.conv; import std.ctype; import std.file; import std.path; import std.stream; import std.string; import std.c.stdio; import std.stdio; alias std.ctype.isdigit isdigit; version (Win32) { import std.c.windows.windows; } bit helpcalled = false; bit versioncalled = false; bit write = false; bit makesyntax = false; bit comment = false; bit runtime = false; bit checkprivate = false; bit fulldebug = false; bit disableversion = false; bit disabledebug = false; int curopt = 1; int numpaths = 0; int allpaths = 10; int depthlimit = -1; int depth = 0; int numdeps = 0; int alldeps = 10; int firstfile = 1; char [][] depmods; char [][] depfiles; char [][] depdepths; char [][] paths; char [][] argslist; char [][] dbgidentsstr; char [][] veridentsstr; int [][] privatearr; int dbgidentsnum; int veridentsnum; /*! * Static constructor. * Adds version identifier strings to the list used by ddepcheck based on what * is defined by the compiler. */ static this() { version (DigitalMars) { veridentsstr ~= "DigitalMars"; } version (X86) { veridentsstr ~= "X86"; } version (AMD64) { veridentsstr ~= "AMD64"; } version (Windows) { veridentsstr ~= "Windows"; } version (Win32) { veridentsstr ~= "Win32"; } version (Win64) { veridentsstr ~= "Win64"; } version (linux) { veridentsstr ~= "linux"; } version (LittleEndian) { veridentsstr ~= "LittleEndian"; } version (BigEndian) { veridentsstr ~= "BigEndian"; } version (D_InlineAsm) { veridentsstr ~= "D_InlineAsm"; } } /* * Function called when the "help" option is specified. */ void optHelp() { optVersion(); if (!helpcalled) { writefln(); writefln("Syntax: ddepcheck [options] [src ...]"); writefln(" -I[path] Add a path to be searched."); writefln(" -h/--help Prints this help table."); writefln(" -d=[num]/--depth=[num] Limit the depth searched"); writefln(" for dependencies."); writefln(" -m/--make-syntax Print the dependencies using the make"); writefln(" syntax. \"objectfile : src deps\""); writefln(" -V/--version Prints the version."); writefln(" -w/--writetofile Prints the dependencies to the file"); writefln(" 'depfile' instead of to the console."); writefln(" -l/--checkruntimelib Tries to add runtimelib dependencies."); writefln(" Note that the path must be added "); writefln(" explicitly."); writefln(" --checkprivate Ignore private if it affects the import"); writefln(" -debug=[ident/num] Uses the same switch as the compiler"); writefln(" to remove/include debug codeblocks."); writefln(" -version=[ident/num] Uses the same switch as the compiler"); writefln(" to remove/include version codeblocks."); writefln(" -disableVersion Don't check for version statements."); writefln(" (All imports are checked.)"); writefln(" -disableDebug Don't check for debug statements."); writefln(" (All imports are checked.)"); } helpcalled = true; } /* * Function called when the "checkruntimelib" option is specified. */ void optCheckRuntime() { runtime = true; } /*! * Function called when the "checkprivate" option is specified. */ void optCheckPrivate() { checkprivate = true; } /*! * Function called if the "version" option is used. */ void optVersion() { if (!versioncalled) { writefln("ddepcheck"); writefln("Dependency walker for D source files."); writefln("Version 1.0.2 Copyright Lars Ivar Igesund 2003 - 2005"); } versioncalled = true; } /*! * Function called if the "writetofile" option is used. */ void optWrite() { write = true; } /*! * Function called if the "depth" option is used to decide the maximum * depth for recursion. */ void optDepth(int arg, bit doubledash) { if (doubledash) { depthlimit = cast(int)atoi(argslist[arg][8..argslist[arg].length]); } else { depthlimit = cast(int)atoi(argslist[arg][3..argslist[arg].length]); } } /*! * Function called for all the "-I" import options used. */ void optImport(int arg) { if (argslist[arg].length == 2) { return; } addPath(argslist[arg][2..argslist[arg].length]); } /*! * Function called for all the "-debug" options used. */ void optDebug(int arg) { if (argslist[arg].length == 6) { fulldebug = true; } else { char [] ident = argslist[arg][7..argslist[arg].length]; bit num = isNumber(ident); if (num) { dbgidentsnum = toInt(ident); } else { dbgidentsstr ~= ident; } } } /*! * Function called for all the "-version" options used. */ void optVersion(int arg) { if (argslist[arg].length == 2) { return; } char [] ident = argslist[arg][9..argslist[arg].length]; bit num = isNumber(ident); if (num) { veridentsnum = toInt(ident); } else { veridentsstr ~= ident; } } /*! * Function called when the "-m"/"--make-syntax" option is used. */ void optMakeSyntax() { makesyntax = true; } /*! * Function called when the "-disableVersion" option is used. */ void optDisableVersion() { disableversion = true; } /*! * Function called when the "-disableDebug" option is used. */ void optDisableDebug() { disabledebug = true; } /*! * Function that checks if an argument is an option. */ bit checkOption(int arg) { switch (argslist[arg]) { case "--version": case "-V": optVersion(); return true; break; case "--help": case "-h": optHelp(); return true; break; case "--writetofile": case "-w": optWrite(); return true; break; case "--make-syntax": case "-m": optMakeSyntax(); return true; break; case "--checkruntimelib": case "-l": optCheckRuntime(); return true; break; case "--checkprivate": case "-p": optCheckPrivate(); return true; break; case "--disableVersion": optDisableVersion(); return true; break; case "--disableDebug": optDisableDebug(); return true; break; default: break; } if (argslist[arg].length > 3 && cmp(argslist[arg][0..3], "-d=") == 0) { optDepth(arg, false); return true; } else if (argslist[arg].length > 8 && cmp(argslist[arg][0..8], "--depth=") == 0) { optDepth(arg, true); return true; } if (argslist[arg].length > 2 && cmp(argslist[arg][0..2], "-I") == 0) { optImport(arg); return true; } if (argslist[arg].length > 5 && cmp(argslist[arg][0..6], "-debug") == 0) { optDebug(arg); return true; } if (argslist[arg].length > 8 && cmp(argslist[arg][0..8], "-version") == 0) { optVersion(arg); return true; } return false; } /*! * Function that starts the dependency walking for base source files. */ void depBase(int arg) { addNewBaseFile(argslist[arg]); depWalk(argslist[arg], "", true); } /*! * Function that do the main dependency walking recursively. If it find * files that has been walked before, it skips it to avoid infinite * cyclic dependencies. */ void depWalk(char [] file,char [] mod, bit base) { debug writefln("Filename: ", file); char [] filepath = file; char [] src; int pathnum = 0; int [] importpos; importpos.length = 0; int nextimport = -1; char [][] verspecstrings; char [][] dbgspecstrings; while (pathnum < numpaths && !fileExist(filepath)) { filepath = std.path.join(paths[pathnum], file); pathnum++; } try { src = (new File(filepath)).toString(); } catch (Error e) { return; } if (!base) { if (!addDep(filepath, mod, depth)) { return; } } stripComment(src); bit useVersion(int pos) { int i = pos + 7; int j; while (src[i] != '(') { i++; } j = i + 1; while (src[j] != ')') { j++; } char [] ident = src[i + 1..j]; if (ident.length == 0) { return true; } bit num = true; foreach (char c; ident) { if (!isdigit(c)) { num = false; break; } } if (num) { int identnum = toInt(ident); if (veridentsnum >= identnum) { return true; } } else { foreach (char [] id; veridentsstr) { if (id == ident) { return true; } else if (id == "none") { return false; } } } return false; } void search(int a, int z) { int dbgfound = -1; int verfound = -1; int impfound = -1; int privfound = -1; int dbgend = -1; int verend = -1; int i = a; bit moreimports = false; bit keepprivate = false; bit useDebug(int pos) { int i = pos + 5; int j; if (i >= src.length) { return false; } while (i < (src.length - 1) && isspace(src[i])) { i++; } if (src[i] == '(') { j = i + 1; while (j < (src.length - 1) && src[j] != ')') { j++; } char [] ident = src[i + 1..j]; bit num = true; foreach (char c; ident) { if (!isdigit(c)) { num = false; break; } } if (num) { int identnum = toInt(ident); if (dbgidentsnum <= identnum) { return true; } } else { foreach (char [] id; dbgidentsstr ~ dbgspecstrings) { if (id == ident) { return true; } } } if (fulldebug) { return false; } } else { if (fulldebug) { return true; } } return false; } bit isKeyword(int startpos, int length) { if (startpos < 0) { debug writefln("- isKeyword startpos < 0."); return false; } if (startpos + length >= src.length) { return false; } debug writefln("- Checking keyword ", src[startpos..(startpos + length)]); if (startpos == 0 || isspace(src[startpos - 1]) || src[startpos] == ';') { debug writefln("- isKeyword: inside first nested if."); if (((startpos + length) == src.length) || isspace(src[startpos + length]) || (src[startpos..startpos + length] == "debug" && src[startpos + length] == ':')) { return true; } } return false; } int findBlockEnd(int pos) { int levels = 0; debug writefln("- Finding block end after pos ", pos); int j = pos; if (pos >= src.length) { return 0; } while (j < (src.length - 1) && isspace(src[j])) { j++; } int findEnd() { while (isspace(src[j])) { j++; } if (src[j] == '{') { levels++; while (levels) { j++; if (src[j] == '{') { levels++; } else if (src[j] == '}') { levels--; } } return j; } else { return 0; } } int retval = -1; if (src[j] != '(') { retval = findEnd(); debug writefln("- Found end at pos ", retval); return retval; } else { j++; while (src[j] != ')') { j++; } j++; } retval = findEnd(); debug writefln("- Found end at pos ", retval); return retval; } int findAttrEnd(int pos, bit priv = false) { int j = pos; if (pos >= src.length) { return 0; } while (j < (src.length - 1) && isspace(src[j])) { j++; } if (src[j] == ':') { while (j < (src.length - 1) && src[j] != '}') { j++; } int pub = find(src[pos..j], "public"); int prot = find(src[pos..j], "protected"); int best = -1; if (isKeyword(pub, 6)) { best = pub; } if (prot > -1 && prot < pub) { if (isKeyword(prot, 9)) { best = prot; } } if (best > -1) { return best; } else { return j; } } else { return 0; } } int findDebugAndVersionEnd(int pos, bit ver, bit other) { int findSpecEnd(int pos) { if (other) { return 0; } int j = pos; if (pos >= src.length) { return 0; } while (j < (src.length - 1) && isspace(src[j])) { j++; } if (src[j] == '=') { int startidx = -1; j++; while (j < (src.length - 1) && src[j] != ';') { if (startidx == -1 && isalnum(src[j])) { startidx = j; } j++; } char [] ident = src[startidx..j]; if (ident.length > 0) { bit num = isNumber(ident); if (ver) { if (num) { try { veridentsnum = toInt(ident); } catch (Exception e) { // Catching top exception here due to strange naming // conventions in Phobos. larsivi 20040724 return j; } } else { verspecstrings ~= ident; } } else { if (num) { try { dbgidentsnum = toInt(ident); } catch (Exception e) { // Catching top exception here due to strange naming // conventions in Phobos. larsivi 20040724 return j; } } else { dbgspecstrings ~= ident; } } } return j; } return 0; } int findStmntEnd(int pos) { int j = pos; if (pos >= src.length) { return 0; } while (j < (src.length - 1) && src[j] != ';') { j++; } return j; } int i = pos; int res = 0; if (i >= src.length) { return -1; } if (other) { i += 4; } else if (ver) { i += 7; } else { i += 5; } if (cast(bit)(res = findSpecEnd(i))) { return res; } else if (cast(bit)(res = findBlockEnd(i))) { return res; } else if (cast(bit)(res = findAttrEnd(i))) { return res; } else if (cast(bit)(res = findStmntEnd(i))) { return res; } else { return -1; } } int findElse(int pos, bit ver, inout bit elseif) { int i = pos; while (isspace(src[i++])) { } if (src[i..i + 4] == "else") { i += 4; while (isspace(src[i++])) { } if (ver) { if (src[i..i + 7] == "version") { elseif = true; } } else { if (src[i..i + 5] == "debug") { elseif = true; } } return findDebugAndVersionEnd(i, ver, true); } else { return -1; } } int checkDebugAndVersion(int pos, bit ver) { int i = pos + (ver ? 7 : 5); int findElseifStart(int pos) { int i = pos; while (isspace(src[i++])) { } if (src[i..i + 4] == "else") { i += 4; while (isspace(src[i++])) { } } else { return -1; } return i; } debug { if (ver) { writefln("- Checking if version identifier at ", pos, " is set."); } else { writefln("- Checking if debug identifier at ", pos, " is set."); } } int elseend = -1; int end = findDebugAndVersionEnd(pos, ver, false); int found = -1; bit elseif = false; if (end == -1) { debug writefln("- No end found."); return i; } elseend = findElse(end, ver, elseif); debug writefln("- Value of elsend is ", elseend); debug { if (elseif) { writefln("- Value of elseif is true"); } } if (ver ? useVersion(pos) : useDebug(pos)) { debug writefln("- Using primary debug/version part."); search(i, end); while (elseif) { elseend = findElse(elseend, ver, elseif); } if (elseend > -1) { return elseend + 1; } else { debug writefln("- Ending checkDebugAndVersion with value ", end + 1); return end + 1; } } else if (elseend > -1) { while (elseif) { int elseifstart = findElseifStart(end); debug writefln("- Found elseif start at pos ", elseifstart); if (ver ? useVersion(elseifstart) : useDebug(elseifstart)) { search(elseifstart, elseend); while (elseif) { elseend = findElse(elseend, ver, elseif); } return elseend + 1; } end = elseifstart; elseend = findElse(elseend, ver, elseif); } search(end + 1, elseend); return elseend + 1; } debug writefln("- Version identifier not set."); return i + 1; } int handleDebug(int pos) { return checkDebugAndVersion(pos, false); } int handleVersion(int pos) { return checkDebugAndVersion(pos, true); } int handleImport(int pos) { bit checkPrivate(int pos) { foreach (int [] positions; privatearr) { if (pos > positions[0] && pos < positions[1]) { return true; } } return false; } debug writefln("- Handling import at position ", pos); int endpos; int i = pos + 8; while (true) { if (src[i] == ',') { // several imports following an 'import' keyword moreimports = true; break; } else if (src[i] == ';') { // last import in a list moreimports = false; break; } i++; if (i == src.length) { return i; } } endpos = i; if (!base && !checkprivate) { if (pos > 7) { if (keepprivate) { debug writefln("- Ending due to private import"); return endpos + 1; } else if (checkPrivate(pos)) { debug writefln("- Ending due to private import"); return endpos + 1; } i = pos - 1; while (isspace(src[i]) && i > 7) { i--; } if (i >= 7 && src[i - 6..i + 1] == "private") { keepprivate = moreimports ? true : false; debug writefln("- Ending due to private import"); return endpos + 1; } } } char [] mod = strip(src[pos + 6..endpos]); debug writefln("- Handling import ", mod); if (!checkIfModule(mod)) { return endpos + 1; } if (!runtime) { if (checkPhobos(mod)) { return endpos + 1; } } char [] fp = createFilePath(mod); depth++; if (depthlimit == -1 || depth <= depthlimit) { depWalk(fp, mod, false); } depth--; debug writefln("- Ending handling import ", mod, " at pos ", endpos); return endpos + 1; } int delegate(int) handler; int best; do { handler = null; best = z + 1; uint belowz = z; if (!checkprivate) { int privstart = -1; do { int privend = 0; debug writefln("- Searching for private from ", i, " to ", z); privfound = find(src[i..z], "private"); if (privfound > -1) { int privstart = privfound + i; if (isKeyword(privstart, 7)) { privend = findBlockEnd(privstart); if (!privend) { privend = findAttrEnd(privstart, true); } if (privend) { privatearr[privatearr.length][0] = privstart; privatearr[privatearr.length][1] = privend; } } } } while (privstart > -1); } debug writefln("- Searching for import from ", i, " to ", z); impfound = find(src[i..z], "import"); if (impfound > -1) { impfound += i; belowz = impfound; } if (!disabledebug) { debug writefln("- Searching for debug from ", i, " to ", belowz); dbgfound = find(src[i..belowz], "debug"); if (dbgfound > -1) { dbgfound += i; belowz = dbgfound < belowz ? dbgfound : belowz; } } if (!disableversion) { debug writefln("- Searching for version from ", i, " to ", belowz); verfound = find(src[i..belowz], "version"); if (verfound > -1) { verfound += i; } } if (impfound > -1) { debug writefln("- Value of impfound is ", impfound, " and i is ", i); if (isKeyword(impfound, 6)) { best = impfound; debug writefln("- Import found at pos ", best); handler = &handleImport; } } if (!disabledebug) { if (dbgfound > -1 && (dbgfound) < best) { if (isKeyword(dbgfound, 5)) { best = dbgfound; debug writefln("- Debug found at pos ", best); handler = &handleDebug; } } } if (!disableversion) { if (verfound > -1 && (verfound) < best) { if (isKeyword(verfound, 7)) { best = verfound; debug writefln("- Version found at pos ", best); handler = &handleVersion; } } } if (!(handler is null)) { i = handler(best); while (moreimports) { i = handler(i); } } else { break; } } while (i < z); } int pos = 0; int found = -1; search(0, src.length); dbgidentsstr ~= dbgspecstrings; veridentsstr ~= verspecstrings; } /*! * Function that adds a dependency to the list when it is verified. * A path and the depth where it was found is also added. If the * dependency has been found before, the new depth is added to the * same entry. */ bit addDep(char [] filepath, char [] mod, int depth) { for (int i = 0; i < numdeps; i++) { if (depmods[i] == mod) { depdepths[i] ~= ","; depdepths[i] ~= toString(depth); return false; } } numdeps++; if (numdeps > alldeps) { alldeps *= 2; depmods.length = alldeps; depfiles.length = alldeps; depdepths.length = alldeps; } depmods[numdeps - 1] = mod; depfiles[numdeps - 1] = filepath; depdepths[numdeps - 1] = toString(depth); return true; } /*! * Function that adds the filename of the current checked file to * the printout. */ void addNewBaseFile(char [] file) { numdeps += 3; if (numdeps > alldeps) { alldeps *= 2; depmods.length = alldeps; depfiles.length = alldeps; depdepths.length = alldeps; } depmods[numdeps-3] = "#"; depmods[numdeps-2] = file; depmods[numdeps-1] = "##"; depfiles[numdeps-3] = depfiles[numdeps-2] = depfiles[numdeps-1] = ""; depdepths[numdeps-3] = depdepths[numdeps-2] = depdepths[numdeps-1] = ""; } /*! * Function that takes a module name and creates a real path. */ char [] createFilePath(char [] mod) { char [] tempmod; tempmod = replace(mod, ".", sep); tempmod ~= ".d"; return tempmod; } /*! * Checks if the found dependency is a part of phobos in which case * it is ignored. */ bit checkPhobos(char [] mod) { if (mod == "Object") { return true; } else if (mod.length < 4) { return false; } else if (cmp(mod[0..4], "std.") == 0) { return true; } else { return false; } } /*! * Checks if the found module really can be a module since the parsing * is a bit hackish. */ bit checkIfModule(char [] mod) { if (!isalpha(mod[0])) { return false; } foreach (char c; mod[1..mod.length]) { if (!(isalnum(c) || c == '.')) { return false; } } return true; } /*! * Adds a path given with "-I" to the path list. */ void addPath(char [] path) { numpaths++; if (numpaths > allpaths) { allpaths *= 2; paths.length = allpaths; } paths[numpaths - 1] = path; } /*! * Checks if a file exist. */ bit fileExist(char [] file) { version (Win32) { return (GetFileAttributesA(toStringz(file)) != 0xFFFFFFFF); } else { // FIXME: I guess there is a better way to do this. larsivi 18042004 bit result = true; try { read(file); } catch (FileException e) { result = false; } return result; } } /*! * Checks if the string is a number. Returns true if it is. */ bit isNumber(char [] ident) { foreach (char c; ident) { if (!isdigit(c)) { return false; } } return true; } /*! * Convert a substring in src to whitespace. */ void toWhiteSpace(inout char [] src, int start, int end) in { assert (end > start); assert (start >= 0); assert (end <= src.length); } body { src[start..end] = ' '; } /*! * Strip off the comments. */ void stripComment(inout char[] src) { int len = src.length; int doubleslash = -1; int star = -1; int plus = 0; int plusstart = -1; for (int i = 0; i < len; i++) { if (doubleslash > -1) { if (src[i] == '\\') { if (src[i + 1] == 'n') { toWhiteSpace(src, doubleslash, i + 2); i++; doubleslash = -1; continue; } } continue; } else if (star > -1) { if (src[i] == '*') { if (src[i + 1] == '/') { toWhiteSpace(src, star, i + 2); i++; star = -1; continue; } } continue; } else if (plus) { if (src[i] == '+') { if (src[i + 1] == '/') { i++; plus--; if (!plus) { toWhiteSpace(src, plusstart, i + 1); plusstart = -1; } continue; } } } if (src[i] == '/') { i++; if (src[i] == '/') { doubleslash = i - 1; } else if (src[i] == '*') { star = i - 1; } else if (src[i] == '+') { if (!plus) { plusstart = i - 1; } plus++; } } } } /*! * Writes dependencies to file. */ void printToFile() { File depfile = new File("depfile", FileMode.Out); for (int i = 0; i < numdeps; i++) { depfile.printf(depmods[i] ~ " " ~ depfiles[i] ~ " " ~ depdepths[i] ~ "\n"); } depfile.close(); } /*! * Prints dependencies to stdout. */ void printToStdout() { if (makesyntax) { char [] objsuf = ""; version (Win32) { objsuf = ".obj"; } version (linux) { objsuf = ".o"; } writef(replace(argslist[firstfile], ".d", objsuf), " : "); writef(argslist[firstfile]); if (numdeps > 3) { writef(" "); for (int i = 3; i < numdeps; i++) { writef(depfiles[i], " "); } } } else { for (int i = 0; i < numdeps; i++) { writefln(depmods[i], " ", depfiles[i], " ", depdepths[i]); } } } int main(char[][] args) { if (args.length == 1) { optHelp(); return 0; } depmods.length = 10; depfiles.length = 10; depdepths.length = 10; paths.length = 10; argslist = args; while (curopt < args.length && checkOption(curopt)) { curopt++; } if (curopt < args.length) { firstfile = curopt; for (int i = curopt; i < args.length; i++) { // FIXME: Handle multiple input files better // larsivi 20040715 depBase(i); } } if (write) { printToFile(); } else { printToStdout(); } return 0; }