Make unused public methods private in components and directives

dreitzner

Domenik Reitzner

Posted on July 17, 2020

Make unused public methods private in components and directives

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
        }));
};
Enter fullscreen mode Exit fullscreen mode

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('./'));
};
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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,
);
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
dreitzner
Domenik Reitzner

Posted on July 17, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related