0

I've got the following function that takes in an object as well as a word and returns an object by replacing all occurrences of the passed word by another string. The same function will count the number of times the word has been replaced and will include that in the replaced string.

For example, consider the following:

  let noOfReplacements = 0;
  const reg = new RegExp(`\\bplease\\b`, "gi");

  const data = [
    {
      name: "please, I said please",
      title: "Test One",
      more: [
        {
          name: "another name",
          title: "please write something. I said please."
        }
      ]
    },
    { name: "testTwo", title: "Test Two" },
    { name: "testThree", title: "Test Three" },
    { name: "testFour", title: "Test Four" }
  ];

  const replace = (obj, reg) => {
    for (const key in obj) {
      if (typeof obj[key] === "object" && obj[key] !== null) {
        replace(obj[key], reg);
      } else if (key === "title") {
        obj[key] = obj[key].replace(reg, function () {
          noOfReplacements += 1;
          return `repalced${noOfReplacements}`;
        });
      }
    }
    return obj;
  };

  const result = replace(data, reg)

  console.log("number of replacements", noOfReplacements);
  console.log("result", result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

This works fine. However, I get an ESlint warning:

Function declared in a loop contains unsafe references to variable(s) 'noOfReplacements'

How can I safely update the noOfReplacements in this case?

I have seen this example, but the problem with that solution is that it doesn't consider separate sentences and separately replaced words. So in the above example, while it will successfully replace all the words, it will only increase the count by 1 since both words are in the same property. Therefore, the noOfReplacements remains at 1 when it should be 2.

Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
Adam
  • 2,384
  • 7
  • 29
  • 66
  • You already do. Ignore this warning. – Jonas Wilms Jan 23 '22 at 12:24
  • The problem is that I can't ignore the warning for certain reasons. – Adam Jan 23 '22 at 12:26
  • Why does the OP choose to `map` when the original structure gets mutated anyhow by a tree walking recursive approach? Does the OP want to have as a result the mutated original data structure or a true changed (copied and replaced) copy of it? – Peter Seliger Jan 23 '22 at 12:50
  • @PeterSeliger you are correct on that front. I've edited the question because I don't need the `map` since I'm taking a recursive approach like you said. However, the warning still persists. – Adam Jan 23 '22 at 12:55
  • You can simply follow the title of the rule and declare your function outside of the loop, and then call it in the loop. – pilchard Jan 23 '22 at 13:02
  • @pilchard you're absolutely right. That was a very simple answer! If you'd like to put your comment as an answer, I'll happily accept. – Adam Jan 23 '22 at 13:09

2 Answers2

1

You can simply follow the title of the rule (no-loop-func) and declare your function outside of the loop, and then call it in the loop.

const replace = (object, reg) => {
  const incrementAndReplace = () => {
    noOfReplacements += 1;
    return `repalced${noOfReplacements}`;
  };

  for (const key in object) {
    if (typeof object[key] === 'object' && object[key] !== null) {
      replace(object[key], reg);
    } else if (key === 'title') {
      object[key] = object[key].replace(reg, incrementAndReplace);
    }
  }

  return object;
};

let noOfReplacements = 0;
const reg = /\bplease\b/gi;

const data = [
  {
    name: 'please, I said please',
    title: 'Test One',
    more: [
      {
        name: 'another name',
        title: 'please write something. I said please.',
      },
    ],
  },
  {name: 'testTwo', title: 'Test Two'},
  {name: 'testThree', title: 'Test Three'},
  {name: 'testFour', title: 'please Test Four'},
];

const replace = (object, reg) => {
  const incrementAndReplace = () => {
    noOfReplacements += 1;
    return `repalced${noOfReplacements}`;
  };

  for (const key in object) {
    if (typeof object[key] === 'object' && object[key] !== null) {
      replace(object[key], reg);
    } else if (key === 'title') {
      object[key] = object[key].replace(reg, incrementAndReplace);
    }
  }

  return object;
};

replace(data, reg);

console.log('number of replacements', noOfReplacements);
console.log('result', data);
pilchard
  • 12,414
  • 5
  • 11
  • 23
1

I anyhow would always choose an approach where the recursively implemented, title specific, replacement function is agnostic to any outer scope. It instead would run within the context of a bound reference which features e.g. a replacementCount property which internally will be tracked. The update is taken care of by a replacer callback.

The next provided recursive implementation also features Array item and Object entry specific iterations in order to only process an object's own properties unlike the OP's for...in loop which iterates prototypal properties as well.

function recursivelyReplaceTitleAndTrackBoundCount(data, regX) {
  let { replacementCount: count } = this;

  const boundRecursiveReplacer =
    recursivelyReplaceTitleAndTrackBoundCount.bind(this);

  const getTrackedCount = (/*match*/) => `repalced_${ ++count }`;

  if (Array.isArray(data)) {
    data.forEach(dataItem =>
      boundRecursiveReplacer(dataItem, regX)
    );
  } else {
    Object
      .entries(data)
      .forEach(([key, value]) => {
        if (value && (typeof value === 'object')) {

          boundRecursiveReplacer(value, regX);

        } else if ((key === 'title') && regX.test(value)) {

          data[key] = value.replace(regX, getTrackedCount);

          this.replacementCount = count;
        }
      });
  }
}

const data = [{
  name: "please, I said please",
  title: "Test One",
  more: [{
    name: "another name",
    title: "please write something. I said please.",
  }],
}, {
  name: "testTwo",
  title: "Test Two",
  even: {
    some: {
      more: {
        name: "another name",
        title: "please write something. I said please.",
      },
    },
  },
}, {
  name: "testThree",
  title: "Test Three",
}, {
  name: "testFour",
  title: "Test Four, if you ...please?",
}];

const tracker = { replacementCount: 0 };

recursivelyReplaceTitleAndTrackBoundCount
  .call(tracker, data, /\bplease\b/gi);

const { replacementCount } = tracker;

console.log({ data, replacementCount });
.as-console-wrapper { min-height: 100%!important; top: 0; }
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37