Domenik Reitzner
Posted on July 17, 2020
This is part six of seven.
The great clean up
We have looked at many different ways to improve code and build quality of our code base. One of the greatest improvements (at least in my case) was to minify private methods. Today we are going to milk this cow even further and look for unused public methods/members in our code base.
Where are you?
We should be able to find all public methods and members that we use inside of our components and directives in our views/templates. So the first step in finding all unused methods is to find those we actually do use.
A great tool at our disposal for this is regex.
In my case all controller names start with a lower case letter followed by any type of letter and a .
/[a-z]+[a-zA-Z]+\./gm
All my methods/members (after the .
) follow almost the same pattern. They don't have to start with a lower case letter and and can also have _
and -
. So we end up with:
/[a-z]+[a-zA-Z]+\.[a-zA-Z_-]+/gm
I wanted to reduce my list even further and thought about all the allowed symbols that are allowed to be before my expression. So my final glorious reges looks like this
/([ |:|'|"|\{|!|\[])([a-z]+[a-zA-Z]+\.[a-zA-Z_-]+)/gm
Stitching it all together:
// new set to get unique matches across all templates
const stringsFromTemplate = new Set();
const tGatherFromTemplates = () => {
// all your templates
const src = [
packagePath + '/**/*.html',
];
return gulp.src(src, {base: './'})
.pipe(through.obj((file, _, cb) => {
// look through content of each file
const string = file.contents.toString();
// find all matches
const matches = string.match(/([ |:|'|"|\{|!|\[])([a-z]+[a-zA-Z]+\.[a-zA-Z_-]+)/gm);
// add matches to our set
if (matches) matches.forEach((match) => stringsFromTemplate.add(match.substring(1)));
cb();
}));
};
Now we should have a list of all used combinations of controller
.method
(if you are using $ctrl
in your project you need to adapt your regex to take this into account)
I'm gonna fix you up
⚠ this step will edit your files, so be careful
As we might encounter some methods/members, that have to be replaced manually (bad naming convention in files, etc.), I started with setting up some Sets (again 😉) and make a regex searchable string out of my unique strings from Template.
const unusedPublic = new Set();
const unusedManual = new Set();
const tFindUnusedPublic = () => {
const stringToSearchT = [...stringsFromTemplate].join(' ');
// get public from js files
return gulp
.src(packagePath + '/**/*.js', {base: './'})
.pipe(through.obj((file, _, cb) => {
const newFile = file.clone();
let string = newFile.contents.toString();
// magic about to happen...
// overwrite contents of vinyl with new buffer
newFile.contents = Buffer.from(string);
cb(null, newFile);
}))
.pipe(gulp.dest('./'));
};
The approach that I took was to check if the file is a class
and then extract the controller name.
const isClass = string.match(/class [A-Z]/);
if (isClass) {
// components and directices
let baseName = string.match(/controllerAs: '(.*)'/);
if (baseName) {
// get name from capture group
baseName = baseName[1];
// TODOs:
// - get public methods/members
// - replace unused
}
}
So the next step is to extract all public methods/members. I extracted this into it's own function, as I'll reuse it for the services in the upcoming article.
const getPublic = (string) => {
// identation is your friend
// method starts with lower case letter
let matchesPublicMethods = string.match(/^ {4}[a-z][a-z0-9_]+\(/gim) || [];
matchesPublicMethods = matchesPublicMethods
// remove indent
.map((m) => m.slice(4, -1))
// filter out constructor and edge cases
.filter((m) => !(m.match('constructor') || string.match(new RegExp(`function ${m}`, 'gim'))));
// get all members and remove "this."
let matchesPublicMembers = string.match(/this\.[a-z][a-z0-9_]+/gim) || [];
matchesPublicMembers = matchesPublicMembers.map((m) => m.slice(5));
// combine and sort descending by length to circumvent bad naming
const combinedPublic = [...matchesPublicMethods, ...matchesPublicMembers]
.sort((a, b) => b.length - a.length);
return new Set(combinedPublic);
};
All that is left to do is to search and replace unused methods/members and stitch it all together.
const searchInString = (uniquePublic, baseName, inString, file, replace) => {
uniquePublic.forEach((m) => {
const search = `${baseName}.${m}`;
// check if method/member is in generated list
if (!inString.match(search)) {
// add to set
unusedPublic.add(search);
// handle pre and postfixes for manual replacement
if (file.match(new RegExp(`[_a-zA-Z']+${m}`, 'gm'))
|| file.match(new RegExp(`${m}[a-zA-Z0-9']+`, 'gm'))) unusedManual.add(search);
// if replace is enabled replace it
else if (replace) file = file.replace(new RegExp(m, 'gm'), `_${m}`);
};
});
// return modified file
return file;
};
if we put it all together we come up with the following:
const stringsFromTemplate = new Set();
const tGatherFromTemplates = () => {
const src = [
packagePath + '/**/*.html',
];
return gulp.src(src, {base: './'})
.pipe(through.obj((file, _, cb) => {
const string = file.contents.toString();
const matches = string.match(/([ |:|'|"|\{|!|\[])([a-z]+[a-zA-Z]+\.[a-zA-Z_-]+)/gm);
if (matches) matches.forEach((match) => stringsFromTemplate.add(match.substring(1)));
cb();
}));
};
const getPublic = (string) => {
let matchesPublicMethods = string.match(/^ {4}[a-z][a-z0-9_]+\(/gim) || [];
matchesPublicMethods = matchesPublicMethods
.map((m) => m.slice(4, -1))
.filter((m) => !(m.match('constructor') || string.match(new RegExp(`function ${m}`, 'gim'))));
let matchesPublicMembers = string.match(/this\.[a-z][a-z0-9_]+/gim) || [];
matchesPublicMembers = matchesPublicMembers.map((m) => m.slice(5));
const combinedPublic = [...matchesPublicMethods, ...matchesPublicMembers]
.sort((a, b) => b.length - a.length);
return new Set(combinedPublic);
};
const searchInString = (uniquePublic, baseName, inString, file, replace) => {
uniquePublic.forEach((m) => {
const search = `${baseName}.${m}`;
if (!inString.match(search)) {
unusedPublic.add(search);
// handle pre and postfixes
if (file.match(new RegExp(`[_a-zA-Z']+${m}`, 'gm'))
|| file.match(new RegExp(`${m}[a-zA-Z0-9']+`, 'gm'))) unusedManual.add(search);
else if (replace) file = file.replace(new RegExp(m, 'gm'), `_${m}`);
};
});
return file;
};
// find public services that should be private
const unusedPublic = new Set();
const unusedManual = new Set();
const tFindUnusedPublic = () => {
const stringToSearchT = [...stringsFromTemplate].join(' ');
// get public from js files
return gulp
.src(packagePath + '/**/*.js', {base: './'})
.pipe(through.obj((file, _, cb) => {
const newFile = file.clone();
let string = newFile.contents.toString();
const isClass = string.match(/class [A-Z]/);
if (isClass) {
// components and directices
let baseName = string.match(/controllerAs: '(.*)'/);
if (baseName) {
baseName = baseName[1];
const uniquePublic = getPublic(string);
// set to true if you are certain you want to replace
string = searchInString(uniquePublic, baseName, stringToSearchT, string, false);
}
}
// overwrite contents of vinyl with new buffer
newFile.contents = Buffer.from(string);
cb(null, newFile);
}))
.pipe(gulp.dest('./'));
};
const tDebug = (cb) => {
console.log(unusedPublic.size, unusedPublic);
console.log(unusedManual.size, unusedManual);
cb();
};
exports.findUnusedPublic = series(
tGatherFromTemplates,
tFindUnusedPublic,
tDebug,
);
Feel free to comment down below if you can think of any edge cases that are not included (one I already know about is getters and setters).
Coming up next
In the next and final part of this series we will take a look at applying the same procedure to the services, by using some of the functions we implemented today.
Posted on July 17, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.